Skip to content
Zuuppa
Dashboard

Guides

Subscription

Recurring billing. Merchant calls go to api.zuuppa.com; the subscriber completes checkout on a pay.zuuppa.com page you link them to. Full contract in the Subscriptions API.

  1. 1

    Step 1 — create a product and a tier

    TypeScript
    const product = await (await fetch(`${SERVER}/subscription-products`, {
      method: "POST",
      headers: { "Content-Type": "application/json", "Authorization": `Bearer ${API_KEY}` },
      body: JSON.stringify({ name: "Pro Plan", external_id: "plan-pro" }),
    })).json();
    
    const tier = await (await fetch(`${SERVER}/subscription-products/${product.id}/tiers`, {
      method: "POST",
      headers: { "Content-Type": "application/json", "Authorization": `Bearer ${API_KEY}` },
      body: JSON.stringify({
        name: "Monthly",
        price_usd_cents: 1500,      // $15.00 / period
        interval: "month",
        authorization_periods: 12,  // wallet authorizes 12 × $15 up front
      }),
    })).json();
  2. 2

    Step 2 — (optional) register a redirect domain

    Only needed if you set success_url/cancel_url. Hosts must be https.

    TypeScript
    await fetch(`${SERVER}/subscription-domains`, {
      method: "POST",
      headers: { "Content-Type": "application/json", "Authorization": `Bearer ${API_KEY}` },
      body: JSON.stringify({ host: "app.yourstore.com" }),
    });
  3. 3

    Step 3 — create an intent and get the checkout link

    TypeScript
    const intent = await (await fetch(`${SERVER}/subscription-intents`, {
      method: "POST",
      headers: { "Content-Type": "application/json", "Authorization": `Bearer ${API_KEY}` },
      body: JSON.stringify({
        tier_id: tier.id,
        success_url: "https://app.yourstore.com/welcome",
        cancel_url: "https://app.yourstore.com/pricing",
        metadata: { user_id: "u_123" },
      }),
    })).json();
    
    // Send the subscriber here:
    redirect(intent.checkout_url);   // https://pay.zuuppa.com/s/<token>
  4. 4

    Step 4 — the subscriber completes hosted checkout

    On pay.zuuppa.com/s/:token the subscriber connects a wallet and either:

    • signs an approve transaction (when a new on-chain allowance is needed — they need zero SOL; the platform fronts the network fee), or
    • signs a SIWS message (when their existing allowance already covers it).

    The subscription is created as incomplete and armed for its first charge. It becomes active only when that first charge confirms on-chain. You don't need to watch the page — you'll get subscription.* and charge.* webhooks.

  5. 5

    Step 5 — manage the subscription

    Read state and charges any time:

    TypeScript
    const { subscription, delegation } =
      await (await fetch(`${SERVER}/subscriptions/${subId}`, {
        headers: { "Authorization": `Bearer ${API_KEY}` },
      })).json();

    Give the subscriber a self-serve portal:

    TypeScript
    const { portal_url } = await (await fetch(`${SERVER}/subscriptions/${subId}/portal-link`, {
      method: "POST",
      headers: { "Authorization": `Bearer ${API_KEY}` },
    })).json();
    // portal_url → https://pay.zuuppa.com/m/<token>

    At the portal the subscriber can cancel, resume, downgrade, or upgrade. Reducing (cancel/downgrade) applies immediately with no signature. Raising (a pricier tier, or renewing a larger allowance) requires a wallet signature — a merchant-minted portal link cannot raise on its own.

Warning

Raising the amount needs the subscriber's signature

You can change a tier from the API with PATCH /subscriptions/:id { tier_id }, but only to lower or keep the amount; a raise returns 403 wallet_proof_required and must go through the portal. See the signature summary.

© 2026 Zuuppa. All rights reserved.