Skip to main content
Hosted checkout is the no-code path to a paying subscriber. The merchant creates a session server-side, redirects the customer to a Duro-hosted page, and Duro handles identity, payment, tokenisation, and subscription creation. The merchant’s servers never touch card data.

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 kobo1500000 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.
Grant access only after verify returns completed. The redirect callback is a hint, the webhook is the trigger, and the verify call is the proof — wire all three and a single dropped or replayed message never under- or over-delivers.

The token carries the mode

A checkout token is cs_<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:
  1. 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.
  2. Charge the chosen rail.
  3. Create the subscription (active), compute its first period via BillingCycle.
  4. Create the invoice paid, record the PaymentAttempt.
  5. Emit subscription_created and subscription_payment_success — which fan out to the merchant’s webhooks.
  6. 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’s StoreConfig. 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.