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.
Session or link?
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.
Sessions and your payment links
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.