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
Step 1 — create a product and a tier
TypeScriptconst 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
Step 2 — (optional) register a redirect domain
Only needed if you set
success_url/cancel_url. Hosts must be https.TypeScriptawait fetch(`${SERVER}/subscription-domains`, { method: "POST", headers: { "Content-Type": "application/json", "Authorization": `Bearer ${API_KEY}` }, body: JSON.stringify({ host: "app.yourstore.com" }), }); - 3
Step 3 — create an intent and get the checkout link
TypeScriptconst 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
Step 4 — the subscriber completes hosted checkout
On
pay.zuuppa.com/s/:tokenthe 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
incompleteand armed for its first charge. It becomesactiveonly when that first charge confirms on-chain. You don't need to watch the page — you'll getsubscription.*andcharge.*webhooks. - 5
Step 5 — manage the subscription
Read state and charges any time:
TypeScriptconst { subscription, delegation } = await (await fetch(`${SERVER}/subscriptions/${subId}`, { headers: { "Authorization": `Bearer ${API_KEY}` }, })).json();Give the subscriber a self-serve portal:
TypeScriptconst { 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.
Raising the amount needs the subscriber's signature
You can change a tier from the API withPATCH /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.