Checkout sessions
Checkout starts when your server asks Elixpo Pay to create a session with your secret key. We resolve the price from your catalog, return a hosted checkout URL, and you redirect the buyer there. Your app never sees card data and never signs an amount.
Create a session
POST https://payouts.elixpo.com/v1/checkout/sessions
Authorization: Bearer <ELIXPO_PAY_API_KEY>
Content-Type: application/json
{
"tier": "member", // product tier to purchase
"currency": "INR", // we pick the matching catalog price
"customer": {
"uid": "u_123", // buyer id in your namespace
"email": "buyer@example.com" // optional, prefilled at checkout
},
"success_url": "https://blogs.elixpo.com/settings",
"metadata": { "plan": "member" } // optional, echoed onto the session
}Authenticate with your ELIXPO_PAY_API_KEY (secret key). The amount is never sent by you — Elixpo Pay looks up the active price for (tier, currency) in your catalog, so a user can't tamper with the price.
Response
201 Created
{
"id": "cs_…",
"url": "https://payouts.elixpo.com/checkout?session=cs_…",
"amount": 19900, // resolved from the catalog (minor units)
"currency": "INR",
"tier": "member",
"expires_at": "2026-06-17T12:30:00.000Z"
}
// If the resolved price is a recurring tier (autopay), the hosted
// checkout page redirects the buyer to Razorpay's mandate URL
// (rzp.io/i/…) instead of opening the Checkout JS modal. You don't
// need to do anything different on your side — the same /v1/checkout/
// sessions call handles both modes; the price's "type" field decides.Redirect the buyer
// In your app (server-side), when a user upgrades:
const res = await fetch("https://payouts.elixpo.com/v1/checkout/sessions", {
method: "POST",
headers: {
Authorization: "Bearer " + process.env.ELIXPO_PAY_API_KEY,
"Content-Type": "application/json",
},
body: JSON.stringify({
tier: "member",
currency: "INR",
customer: { uid: user.id, email: user.email },
success_url: "https://blogs.elixpo.com/settings",
}),
});
const session = await res.json();
redirect(session.url); // send the buyer to hosted checkoutWhat happens next
- The hosted page loads the session, looks at the resolved price's billing mode (one-time or autopay), and runs the matching flow.
- One-time — we lazily create a Razorpay Order and open Razorpay Checkout. The client signature is verified and we fulfill immediately; the Razorpay webhook re-confirms authoritatively (idempotent — never double-grants).
- Autopay (recurring) — we lazily create a Razorpay Plan (cached per price) and a Subscription, then redirect the buyer to Razorpay's hosted mandate URL. Once they accept,
subscription.activated+subscription.chargedwebhooks fire and the entitlement is granted. - We grant the entitlement, then notify your app (see Webhooks) and redirect the buyer to
success_url. Each renewal charge re-firesentitlement.updatedso your DB stays in sync.
Cancelling a subscription
For autopay tiers your buyer can self-serve cancel from your app, which calls our cancel endpoint:
POST https://payouts.elixpo.com/v1/subscriptions/cancel
Authorization: Bearer <ELIXPO_PAY_API_KEY>
Content-Type: application/json
{
"customer": { "uid": "u_123" }, // same uid passed at checkout
"cancel_at_cycle_end": true // default. false = stop billing now
}Graceful by default: the buyer keeps access through the period they already paid for, then auto-downgrades when the entitlement expires. We fire entitlement.updated with status: "cancelled" immediately so you can email the buyer; and again with active: false at period end so you can flip the tier.
Timeouts & limits to set on your side
- HTTP client timeout: set ≥
20son calls toPOST /v1/checkout/sessionsandPOST /v1/subscriptions/cancel. We do a synchronous round-trip to Razorpay (plan create, subscription create, mandate cancel) on each call; cold-paths can take 6–12s. Anything under 10s will false-timeout under load. The underlying mail.elixpo + Razorpay APIs use the same ~20s convention, so this aligns the whole stack. - Checkout-session lifetime: sessions expire
30 minutesafter creation. The hosted checkout enforces this; a buyer who lingers on the price summary past 30m will hit a friendly "session expired" page and get bounced back to your app's pricing URL. - Autopay mandate lifetime (RBI rule): UPI Autopay and Card eMandate both cap at
30 yearsfrom creation. We clamptotal_countto360(=30y monthly) per RBI; passing higher fails withexpire_at cannot be more than 30 years for upi. Buyers who somehow run through 30 years of renewals re-mint a fresh sub — operationally irrelevant. - Per-charge auth window (UPI): UPI Autopay needs a charge intent 24h before debit; Razorpay handles this internally — your only obligation is to keep the subscription active. No timeouts to set on your side here.
- Webhook delivery: we retry delivery for
up to 7 dayswith exponential backoff. Your endpoint MUST respond within10 secondswith 2xx or we treat the delivery as failed and queue a retry. Don't process inline — ack fast, work async if needed. - Idempotency window: dedup deliveries on
payload.idwithin at least24 hours. Retries reuse the original id; processing the same event twice will double-grant entitlements.
