Checkout Sessions
Create one-off payments programmatically — the only public payments endpoint today.
Checkout Sessions
A Checkout Session is the primary API endpoint for taking a payment. You create it from your server, redirect the customer to checkout_url, and receive a webhook when it's paid.
Base URL: https://api.southbill.com/v1
Auth: Authorization: Bearer sk_live_…
Create a session
POST /v1/checkout/sessions
curl -X POST https://api.southbill.com/v1/checkout/sessions \
-H "Authorization: Bearer sk_live_…" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: order_12345" \
-d '{
"amount": 4990,
"currency": "eur",
"customer_name": "Jane Doe",
"customer_email": "jane@example.com",
"reference": "ORDER-12345",
"description": "Order #12345",
"line_items": [
{ "name": "Sneaker Runner Pro", "quantity": 1, "amount": 4990, "image_url": "https://…/sneaker.jpg", "price_id": "price_1Tu…" }
],
"success_url": "https://shop.example.com/thanks?o=12345",
"cancel_url": "https://shop.example.com/cart"
}'
Body
| Field | Type | Required | Notes |
|---|---|---|---|
amount |
integer | ✅ | Minor units (cents). Minimum 250 (= 2.50 in the currency). |
currency |
string | ✅ | 3-letter ISO, lowercase. |
customer_name |
string | ✅ | 2–120 chars. Needed to attribute the payment. |
customer_email |
string | ✅ | Valid email. Receipt is sent here. |
mode |
string | ➖ | payment (default) or subscription. For subscription, pass line_items[0].price_id pointing at a recurring price and omit amount. |
reference |
string | ➖ | Your order ID. Echoed on receipts and webhooks. |
description |
string | ➖ | Short description shown on the checkout. |
line_items |
array | ➖ | Display only. Fields: name, quantity, amount, image_url, price_id, product_id. |
success_url / cancel_url |
string | ➖ | Where the browser is sent after the session ends. |
metadata |
object | ➖ | Free-form key/value returned in webhooks. |
line_items[].price_id is optional — pass it when the item comes from a product you created in the dashboard, so it appears in receipts and analytics with its catalog link. The actual amount charged is amount — the price is not fetched from the catalog.
Response
{
"id": "cs_01H…",
"object": "checkout.session",
"mode": "payment",
"livemode": true,
"status": "open",
"amount": 4990,
"currency": "EUR",
"client_secret": "cs_01H…_secret_…",
"checkout_url": "https://payments.southbill.com/c/cs_01H…?cs=…",
"embed_url": "https://payments.southbill.com/embed/cs_01H…?cs=…",
"expires_at": 1735776000,
"created": 1735689600
}
Redirect the buyer to checkout_url, or mount embed_url in an iframe.
Hosted vs embedded — your choice, every time
Every Checkout Session response returns three integration handles, for one-time and subscription sessions:
client_secret— for a fully custom SDK UI.checkout_url— hosted page onpayments.southbill.com. Redirect here → the buyer leaves your site.embed_url— the same session, rendered for an iframe. Mount it → the buyer stays on your site.
Southbill does not use a ui_mode parameter. The session is always both hosted and embeddable; you choose at render time:
- Redirect / hosted:
window.location = session.checkout_url - Embedded:
<iframe src="{{embed_url}}" allow="payment *">, or the SDK:southbill.checkout({ mode: 'embed', mount: '#southbill-checkout' })
This is identical for mode: "subscription" — pass line_items[0].price_id pointing at a recurring price, then mount embed_url to keep the subscription checkout inline. Do not redirect to checkout_url if you want it on-site.
A browser redirect or iframe
postMessageis never proof of payment. Always confirm server-side via webhooks (checkout.session.completed, or for subscriptionsinvoice.paid/subscription.created).
List, retrieve, update & expire
GET /v1/checkout/sessions # newest first: ?limit= &starting_after= &status=
GET /v1/checkout/sessions/{id}
PATCH /v1/checkout/sessions/{id} # or POST /v1/checkout/sessions/{id}/update
POST /v1/checkout/sessions/{id}/expire
Update an open session
An open, unconfirmed session (flow version 5+) can be updated instead of recreated — useful when a cart changes quantity. Updatable fields: amount, line_items, description, reference. Requires checkout:write.
curl -X PATCH https://api.southbill.com/v1/checkout/sessions/cs_123 \
-H "Authorization: Bearer sk_live_..." \
-H "Content-Type: application/json" \
-d '{"amount": 5900, "reference": "ORD-1042"}'
{ "id": "cs_123", "object": "checkout.session", "status": "open", "flow_version": 5, "revision": 3, "amount": 5900, "currency": "eur" }
Every successful update increments revision. The call returns 409 when the session cannot be changed: immutable_session (flow version below 5), confirmation_started / confirmation_in_progress (a payment is already running) or revision_conflict (changed concurrently — re-read and retry).
Sessions expire automatically 24 hours after creation. An expired session emits checkout.session.expired.
Idempotency
Send Idempotency-Key: <your-key> on every POST. Keys are scoped to (merchant_id, method, path) and expire after 24 hours.
- Same key + same body → the original response is replayed verbatim (same status, same body).
- Same key + different body →
409 Conflictwith:
{
"error": {
"type": "idempotency_error",
"code": "idempotency_key_reused",
"message": "Idempotency-Key reused with a different request body"
}
}
See Error reference for the full model.
Fees
Fees are calculated in EUR on your active plan and deducted from the merchant's balance in the transaction currency (converted at live FX). See Fees, currency & minimum amounts.
Errors
| HTTP | error.type |
error.code |
Meaning |
|---|---|---|---|
| 400 | invalid_request |
field-specific | Missing / malformed field. |
| 400 | amount_too_small |
amount_below_minimum |
amount below the 2.50 minimum. |
| 401 | authentication_error |
invalid_api_key |
Bad, revoked or wrong-mode key. |
| 402 | card_error |
card_declined |
Buyer's card was declined. |
| 409 | idempotency_error |
idempotency_key_reused |
Same key sent with a different body. |
| 409 | resource_conflict |
merchant_not_ready |
Merchant onboarding incomplete. |
| 429 | rate_limit_error |
rate_limited |
Slow down. Respect Retry-After. |
Button label & wallets
The pay button text is not an API field: it comes from a fixed list in Dashboard → Checkout builder (Pay {amount}, Pay securely, Complete purchase, Buy now, Place order, Donate {amount} for one-time; Subscribe, Subscribe now, Start subscription, Pay subscription for subscriptions). Apple Pay / Google Pay are shown on supporting devices for one-time and subscription sessions; for subscriptions the wallet sheet displays the recurring terms and stores the mandate. See Hosted Checkout.
Variants (catalog options)
A line item can reference a product variant instead of a Stripe price. The variant owns its own mirrored price, so the amount is always resolved server-side — a variant checkout can never be mispriced by the caller.
POST /v1/checkout/sessions
{
"mode": "payment",
"line_items": [{ "variant": "b0f4…-uuid", "quantity": 2 }],
"success_url": "https://example.com/thanks"
}
Rules:
variantmust be a Southbill variant id (uuid) belonging to your merchant account. Mixingvariantandpricein one session is rejected (400).- The variant, its price and its product must all be active, and the price must be one-time. Recurring prices are rejected — use
price_idfor subscriptions; variants are not supported inmode: "subscription". amountandcurrencyare derived from the variant price. If you send them and they do not match, the request fails withamount_mismatch/currency_mismatch.- Each variant may appear only once — use
quantity.
Stock-tracked variants
If the variant has inventory tracking enabled, creating the session reserves the units. If the stock is gone the request fails with 409 out_of_stock and no session is created. A stock-tracked variant must be the only line item of the session; for multi-item carts use the Storefront checkout.
The reservation is consumed when the payment succeeds and released automatically when the session is cancelled, fails or expires. A full refund restores the stock.
The resolved variant_id, product_id and sku are returned on the session line items and included in checkout.session.* webhooks.
Declined attempts in sandbox
A declined test card records a failed payment with failure_code and failure_message and emits payment_intent.payment_failed. The hosted Checkout Session remains open for another card attempt; do not fulfil unless checkout.session.completed is received.