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.
1
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.2
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.3
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.4
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.5
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.6
Verify, then give value
Before provisioning, confirm the truth from Duro with the session
id. Act only when status is completed. The same call backs both the callback and the webhook — so a dropped webhook or a closed browser never costs you a fulfilment, and a replay never double-grants.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.