Elixpo Pay

Elixpo Docs

Dashboard

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

http
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

json
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

javascript
// 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 checkout

What 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.charged webhooks 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-fires entitlement.updated so 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:

http
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 ≥ 20s on calls to POST /v1/checkout/sessions and POST /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 minutes after 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 years from creation. We clamp total_count to 360 (=30y monthly) per RBI; passing higher fails with expire_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 days with exponential backoff. Your endpoint MUST respond within 10 seconds with 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.id within at least 24 hours. Retries reuse the original id; processing the same event twice will double-grant entitlements.

Catalog syncWebhooks