Create a checkout session
Open a multi-item checkout session and redirect the buyer to pay.
A checkout session collects payment for up to ten items at once. You open the session, send the buyer to the checkout URL it returns, SUQO runs the whole payment flow, and you learn the outcome from a webhook.
/api/v1/checkout-sessions/Opens a session valid for about 30 minutes and returns a URL for the buyer to pay. This is the recommended way to collect payment — it replaces Create a subscription, which stays supported for integrations already built on it.
The flow
The two arrows at the end matter separately: the redirect tells you where to put the buyer next, and the webhook tells you whether money moved. Only the second one is proof.
Before you start
Decide how the buyer is identified. There are two paths and they give the buyer a different checkout:
customer_id | What the buyer sees |
|---|---|
| Sent | Lands directly on the payment selector. Their details are already known and are not collected again. |
Omitted, null or "" | Fills in their full name, email, phone and address at checkout. All four are required. The address they enter is used for billing — there is no separate billing or shipping address to collect. |
To take the first path, record the buyer with Create a customer and pass the cus_… it returns.
customer_id cannot be edited at checkout. The buyer has no way to correct a typo in their own name or address from the payment page. Fix it with Update a customer and open a new session.For catalogue items, get a pbp_id. Fetch it from Products; every billing period in billing_periods[] carries one.
Request body
| Field | Type | Required | Description |
|---|---|---|---|
items | array | Yes | One to ten items — see Items |
return_url | string (URL) | Yes | Where the buyer lands after paying. Stored and echoed back verbatim |
customer_id | string | No | A cus_… already on your account. null or "" is the same as omitting it |
Items
Each element of items[] is one of two shapes. Send either the catalogue form or the inline form, not a mix of the two within a single item.
Catalogue item
Charges an existing billing period at the price in your catalogue.
| Field | Type | Required | Description |
|---|---|---|---|
pbp_id | string | Yes | Billing period to charge, from Products |
Partner-priced item
Charges a price you set, with no billing period behind it. Use it for anything not in your SUQO catalogue.
| Field | Type | Required | Description |
|---|---|---|---|
name | string | Yes | What the buyer sees on the checkout page |
amount | decimal string | Yes | The price to charge |
interval_type | string | Yes | day, week, month, year or one_time |
interval_count | integer | Yes | Cadence multiplier — 1 with month bills monthly. Send 0 when interval_type is one_time |
one_time is a single purchase rather than a cadence, so there is nothing to multiply — send interval_count as 0. It has no next billing date, it is left out of the counters on Subscriptions, and what it creates cannot be cancelled, resumed or rescheduled.
amount must already include discounts and any applicable charges. SUQO does not compute them for a partner-priced item — it charges exactly the number you send, so apply any discount before you send it. There is no separate discount field.Prices are snapshotted when the session opens. Editing a plan afterwards cannot change what the buyer was shown, so a session already sent out stays honest.
A session holds one to ten items. Fewer than one or more than ten is rejected — see Errors.
One item per product — the last one wins. If two items resolve to the same product, only the last of them is kept. Two different billing periods of one product is the realistic way to hit this: sending [{"pbp_id": "pbp_1"}, {"pbp_id": "pbp_2"}] where both belong to the same product opens a session carrying pbp_2 alone. No error is raised. If your cart lets a buyer pick, say, both the Monthly and the Yearly billing period of one product, collapse that choice before you call this endpoint — otherwise one of them disappears silently.
Example request
{
"customer_id": "cus_2a108b4eb",
"return_url": "https://partner.example.com/orders/1234",
"items": [
{ "pbp_id": "pbp_3n9k2x" },
{
"name": "Onboarding setup",
"amount": "2500.00",
"interval_type": "one_time",
"interval_count": 0
}
]
}Response
Status: 201 Created
| Field | Type | Description |
|---|---|---|
public_id | string | Session id (cks_…) — also the session_id on the webhook |
expires_at | datetime | When the checkout URL stops working |
checkout_url | string (URL) | Send the buyer here |
{
"public_id": "cks_3n9k2xQ7m",
"expires_at": "2026-07-03T10:45:00Z",
"checkout_url": "https://app.suqo.ai/checkout/cks_3n9k2xQ7m"
}checkout_url. Send the customer to that
URL — SUQO handles the entire payment flow (collecting details, wallet/bank payment) and
returns them to your return_url afterward. You don't process the payment yourself. The URL stops
working at expires_at, about 30 minutes out, so open a fresh session rather than caching one.When the buyer comes back
SUQO returns the buyer to your return_url with a status appended:
https://partner.example.com/orders/1234?status=success
https://partner.example.com/orders/1234?status=failureThis parameter is for your UI and nothing else — showing a thank-you page or a try-again page. By the time it reaches you it has passed through the buyer's browser and can be typed by hand. Never mark an order paid, provision access or release goods on the strength of it. Every decision that costs you something follows the webhook below.
Get the payment result
The webhook is the source of truth. SUQO POSTs it to your endpoint for both cases:
| Outcome | Event |
|---|---|
| Payment succeeded | checkout.succeeded |
| Payment failed — declined or abandoned | checkout.failed |
One delivery per session, whatever the item count. The body is keyed by session_id — the public_id you got back when you opened the session — and carries an items array echoing what the session holds:
{
"event": "checkout.succeeded",
"session_id": "cks_3n9k2xQ7m",
"amount": "5000.00",
"status": "succeeded",
"items": [
{ "pbp_id": "pbp_3n9k2x" },
{
"name": "Onboarding setup",
"amount": "2500.00",
"interval_type": "one_time",
"interval_count": 0
}
]
}Don't mark an order paid until you receive checkout.succeeded. See Receive webhook events to set the endpoint up, and Webhook events for the full payloads.
Rate limit
This is the only rate-limited endpoint on the API: 20 sessions a minute per account. Past that, requests are answered with 429 and a message naming the wait:
{
"detail": "Request was throttled. Expected available in 41 seconds."
}Open a session when the buyer commits to paying, not when they load a page — that keeps a busy catalogue well inside the limit. See Errors → Rate limits.
Errors
The 400s specific to this endpoint are listed under Errors → Creating a checkout session.