The lifecycle of a session
Integrate in six steps
Redirect checkout is server-to-server plus one browser redirect. Your secret key (sk_…) never leaves your server; the customer only touches the Duro-hosted page.
Set your keys
Copy your keys from Developers → API keys. The prefix sets the mode —
sk_test_… / pk_test_… for sandbox, sk_live_… / pk_live_… for live. Keep the secret key server-side only.Create a checkout session (server)
POST /v1/checkout/sessions with a planId (or a one-off amount in kobo). Duro returns the session id, a token, and the hosted url. Pass a successUrl to choose where the customer lands afterward, and a metadata object to carry your own order id all the way through to the webhook.Response
Amounts are integer kobo —
1500000 is ₦15,000. Store data.id against your order; it’s what you verify with later.Redirect the customer
Send the browser to
session.url. Duro handles identity, card capture, rail selection, and subscription creation — your servers never see card data.Handle the return (callback)
When payment resolves, Duro returns the customer to your
successUrl with the reference appended. Treat this as a UI hint only — render a “confirming…” screen, never grant access here. A browser can be closed, replayed, or spoofed; the webhook and the verify call are the real triggers.Receive the webhook
Subscribe your endpoint in Developers → Webhooks and select
subscription_payment_success (and subscription_created). Verify the signature against the raw body, dedupe on Duro-Event-Id, and return 200 fast — do the slow work afterward.The token carries the mode
A checkout token iscs_<mode>_<random>. The public hydrate/pay/status endpoints are unauthenticated — they’re hit by a customer’s browser with no API key — so the mode can’t come from a key prefix. It comes from the token itself: the service parses cs_test_ vs cs_live_ and queries exactly one schema. (An earlier version scanned both schemas; deriving the mode from the prefix made it a single query and is documented as a perf fix.)
What a successful pay does, atomically
On a successful charge the service does the full subscriber setup in one flow — and reuses an existing customer instead of minting duplicates:- Resolve the customer by email/phone (
findByContact). If they’re blacklisted, the pay is rejected before the charge — no money moves for a banned customer. - Charge the chosen rail.
- Create the subscription (
active), compute its first period viaBillingCycle. - Create the invoice
paid, record thePaymentAttempt. - Emit
subscription_createdandsubscription_payment_success— which fan out to the merchant’s webhooks. - Mark the session
completed, so a second pay is idempotent (it returns the completed result rather than charging again).
Branding comes from the appearance config
The hosted page renders with the merchant’s published appearance config — logo, colours, copy — pulled from the core schema’sStoreConfig. The same token set themes the inline SDK, the customer portal, and the emails, so a merchant brands once. See the appearance builder →
Next: the inline SDK — the same flow as a popup, plus saved-card identity.