Checkout sessions

A checkout session is a payment page made for one purchase by one payer.

Create it on your server when somebody decides to pay, send them to its url, and wait for checkout.session.completed. If that shape looks familiar, it is meant to: it is the one most payments tutorials teach, and there is no reason to make you learn a different one to reach corridors nobody else covers.

The payer needs no account. They can pay from a bank or mobile-money account in eleven African corridors, or from any crypto wallet — and you are credited USD either way.

Both are payment pages. They differ in who they are for.

Payment link Checkout session
Paid by anyone with the URL the one customer you made it for
How many times as many as you allow once
Lifetime until you deactivate it 24 hours by default
Made for an invoice you email, a bio link a cart somebody is standing in front of

Use a link when you would put the URL in a message. Use a session when your server already knows who is paying and what for.

Open a session

POST /v1/checkout/sessions
{
  "reference": "invoice-4471",
  "title": "Acme Studio",
  "line_items": [
    { "name": "Design retainer — August", "amount": 20 },
    { "name": "Rush fee", "amount": 5 }
  ],
  "collect": ["email", "billing_address"],
  "success_url": "https://acme.com/thanks?session={CHECKOUT_SESSION_ID}",
  "cancel_url": "https://acme.com/cart"
}
{
  "id": "K7pQ2rX9tLmA4vB1",
  "url": "https://pay.spendfigo.com/pay/K7pQ2rX9tLmA4vB1",
  "embed_url": "https://pay.spendfigo.com/embed/K7pQ2rX9tLmA4vB1",
  "status": "open",
  "amount": 25,
  "currency": "USD",
  "reference": "invoice-4471",
  "amount_paid": 0,
  "expires_at": "2026-08-22T09:14:00.000Z"
}

Send Idempotency-Key and a retry cannot open a second session.

The amount

Give an amount, or line_items that add up to one, or both — and if you send both they must agree. A payer shown one figure and charged another has every right to dispute it, so we refuse the request rather than pick a winner.

Line items are shown to the payer and nothing more. We do not price them, tax them or ship them.

reference is required

It is the handle you will find this by, in the API and in your dashboard. We never invent one, because a reference we made up is not something you can reconcile against.

It is not an idempotency key. Send Idempotency-Key for that. One invoice can legitimately have several payments; one request must have one outcome.

Ask the payer for more

collect takes any of email, phone, name, billing_address. The page asks for them alongside whatever the rail already needs, and will not let the payer continue until they are answered — a merchant who asked for a billing address needs it to ship or to invoice, not afterwards.

The answers come back on the session and on the completion webhook:

"collected": {
  "email": "ada@example.com",
  "billing_address": "12 Marina, Lagos"
}

Only what you asked for is kept.

A phone you ask for here is a contact number. It is not the mobile-money number being charged — the rail collects that separately, and confusing the two would charge the wrong one.

Read it back

GET /v1/checkout/sessions/{id}
status what it means
open can still be paid
complete has been paid
closed expired, or you closed it

Those three are the whole set, and complete is the only one that means paid. There is no paid, completed or succeeded — if you are matching several spellings to be safe, match one and let an unknown value fail loudly instead. A status you do not recognise is a reason to stop, not to credit.

This is what your success page should call. Reading the session is quicker than waiting for the webhook and says the same thing.

Close one early

POST /v1/checkout/sessions/{id}/expire

For a customer who abandoned the cart or paid another way. A session that has already been paid is left alone, and closing one twice is not an error.

When the payer returns

success_url is opened once they have paid, with {CHECKOUT_SESSION_ID} replaced by the session's id. cancel_url is where a payer who gives up is sent.

Neither is proof of payment — a browser can be pointed anywhere. checkout.session.completed, delivered to your server, is the one to bill on.

Inside your own page

embed_url is the same checkout without our header, meant to be framed, for when you would rather your customer never left your site. It reports what happens to the page above it — see Put the checkout inside your own page.

GET /v1/payment-links lists links only. Sessions outnumber them and most expire unpaid; a hundred abandoned checkouts would bury the four links you actually made. Sessions have their own list:

GET /v1/checkout/sessions

The hundred most recent, newest first. It answers the question support gets asked — did this customer's payment land — rather than serving as a list of orders, which is yours to keep. To follow one session, read it by the id you were given when you opened it.

line_items and collect are not session-only — POST /v1/payment-links takes both, and they behave identically, because a link and a session are the same page underneath. An itemised link with no amount is priced by its items, which is how you send an invoice.