Skip to main content

The Complete Flow

Let’s walk through what happens when a user subscribes to your service.

1. User Arrives at Your Website

Your potential customer visits your pricing page and clicks “Subscribe.”
<!-- Your pricing page -->
<button onclick="subscribe()">
  Subscribe to Premium - $9.99/month
</button>

2. You Create a Checkout Session

Your backend calls our API to create a checkout session:
const eventop = require('@eventop/sdk');

app.post('/create-subscription', async (req, res) => {
  const session = await eventop.checkout.create({
    merchantWallet: process.env.MERCHANT_WALLET,
    planId: 'premium-monthly',
    customerEmail: req.user.email,
    customerId: req.user.id, // Your internal user ID
    successUrl: 'https://yourdomain.com/success',
    cancelUrl: 'https://yourdomain.com/pricing',
    metadata: {
      userId: req.user.id,
      source: 'pricing_page'
    }
  });
  
  res.redirect(session.url);
});
What you get back:
{
  "sessionId": "session_1a2b3c4d",
  "url": "https://checkout.eventop.xyz/session_1a2b3c4d",
  "expiresAt": "2024-12-09T10:30:00Z"
}

3. User Lands on Our Checkout Page

The user is redirected to our hosted checkout page at checkout.eventop.xyz.
Checkout Page
The page shows:
  • Your company name and logo (from merchant profile)
  • Plan details and pricing
  • Customer’s email address
  • Call-to-action to continue

4. We Detect User’s Status

Our checkout page automatically detects if the user:
Best case: User already has the Eventop mobile app
  • We immediately deep link to the app
  • User approves subscription with one tap
  • Returns to your site in seconds

5. User Completes Subscription in App

1

App Opens with Deep Link

    eventop://subscribe?sessionId=session_1a2b3c4d
The app loads the checkout session and displays:
  • Your company name
  • Plan details
  • Customer’s email
  • Current wallet balance
2

Balance Check

The app automatically checks if the user has sufficient balance (typically 3 months of subscription fees as a buffer).
  • Sufficient: Proceeds to Step 3
  • Insufficient: Prompts user to add funds first
3

On-Chain Transaction

User taps “Confirm Subscription” and approves the blockchain transaction.This creates:
  • A subscription PDA (Program Derived Address)
  • Links their subscription wallet to your merchant wallet
  • Registers the billing interval
4

Session Completion

The app calls our backend to complete the checkout session:
    POST /api/checkout/{sessionId}/complete
    {
      "subscriptionPda": "ABC123...",
      "userWallet": "7xKXtg2...",
      "signature": "5j7Km..."
    }

6. You Receive a Webhook

Immediately after subscription creation, we send a webhook to your endpoint:
{
  "event": "subscription.created",
  "timestamp": 1701234567890,
  "data": {
    "sessionId": "session_1a2b3c4d",
    "subscriptionId": "ABC123...",
    "customer": {
      "email": "user@example.com",
      "customerId": "user_123",
      "walletAddress": "7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU"
    },
    "plan": {
      "planId": "premium-monthly",
      "planName": "Premium Plan",
      "amount": "9990000", // 9.99 USDC (6 decimals)
      "interval": "2592000" // 30 days in seconds
    },
    "metadata": {
      "userId": "user_123",
      "source": "pricing_page"
    }
  }
}
Always verify webhook signatures! See Webhook Security.
Your webhook handler should:
app.post('/webhooks/eventop', async (req, res) => {
  // 1. Verify signature
  if (!verifySignature(req.body, req.headers['x-webhook-signature'])) {
    return res.status(401).send('Invalid signature');
  }
  
  // 2. Process the event
  const { event, data } = req.body;
  
  if (event === 'subscription.created') {
    // Grant access to your service
    await db.users.update({
      id: data.customer.customerId,
      subscriptionStatus: 'active',
      subscriptionId: data.subscriptionId,
      walletAddress: data.customer.walletAddress
    });
    
    // Send welcome email
    await sendEmail(data.customer.email, 'Welcome to Premium!');
  }
  
  // 3. Return 200 quickly
  res.json({ received: true });
});

7. User Returns to Your Site

The user is redirected back to your successUrl:
https://yourdomain.com/success?session_id=session_1a2b3c4d
Your success page can:
  • Display a confirmation message
  • Show next steps
  • Fetch subscription details from your database
  • Provide access to premium features
Example success page:
// /success page
app.get('/success', async (req, res) => {
  const { session_id } = req.query;
  
  // Optional: Verify session with our API
  const session = await eventop.checkout.getSession(session_id);
  
  if (session.status === 'completed') {
    res.render('success', {
      planName: session.plan.planName,
      customerEmail: session.customer.email
    });
  } else {
    res.redirect('/pricing');
  }
});

Recurring Payments

After the initial subscription, payments happen automatically.

How Automatic Payments Work

1

Payment Becomes Due

Based on the billing interval (e.g., 30 days), the payment becomes due.
2

Our System Executes Payment

Our automated payment scheduler:
  • Detects due payments
  • Executes the on-chain transaction
  • Transfers funds from user’s subscription wallet to your wallet
3

You Receive Webhook

    {
      "event": "subscription.payment_succeeded",
      "timestamp": 1703826567890,
      "data": {
        "subscriptionId": "ABC123...",
        "customer": {
          "email": "user@example.com",
          "walletAddress": "7xKXtg2..."
        },
        "userWallet": "6xKlsjj...",
        "amount": "9990000",
        "paymentNumber": 2,
        "nextPaymentDate": "2025-02-08T10:00:00Z"
      }
    }
4

Your System Extends Access

    if (event === 'subscription.payment_succeeded') {
      await db.users.update({
        id: userId,
        subscriptionPaidUntil: data.nextPaymentDate
      });
    }

What If Payment Fails?

If a user’s subscription wallet has insufficient balance:
  1. We retry automatically (1 min, 5 min, 30 min intervals)
  2. You receive a webhook:
   {
     "event": "subscription.payment_failed",
     "data": {
       "subscriptionId": "ABC123...",
       "amountRequired": "9990000",
       "balanceAvailable": "2000000",
       "failureCount": 1
     }
   }
  1. You can notify the user to add funds
  2. After 3 failures, the subscription becomes inactive

Subscription Lifecycle

Subscription Lifecycle

States

StateDescriptionYour Action
activeSubscription is active and payments are being processedProvide access to services
past_duePayment failed, in retry periodShow warning banner, prompt to add funds
cancelledUser cancelled subscriptionRevoke access (optionally offer grace period)
expiredPayments failed 3 timesRevoke access, offer reactivation

Key Concepts

Session vs Subscription

  • Checkout Session: Temporary (30 min TTL), used for the checkout process
  • Subscription: Permanent on-chain record, represents the active subscription
Once checkout completes, you work with the subscription ID (not session ID).

Customer Identity

Every subscription links three identifiers:
  1. Email: From your system, used for communication
  2. Customer ID: Your internal user ID (optional)
  3. Wallet Address: The user’s Solana wallet (on-chain identifier)
This allows you to map blockchain subscriptions to your existing user database.

Metadata

You can attach custom metadata to checkout sessions:
metadata: {
  userId: '123',
  source: 'pricing_page',
  campaign: 'summer-sale-2024',
  referrer: 'twitter'
}
This metadata:
  • Is included in all webhooks
  • Helps you track conversions and sources
  • Can store any JSON-serializable data

Testing the Flow

Test on Devnet first before going live on Mainnet.
  1. Create a test plan in the dashboard
  2. Use a devnet wallet as your merchant wallet
  3. Create a checkout session with a test email
  4. Download the Eventop app (in devnet mode)
  5. Complete the checkout flow
  6. Verify webhooks are received
  7. Check that the subscription appears in your dashboard
See Testing Guide for detailed instructions.

Next Steps