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 on payments.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 postMessage is never proof of payment. Always confirm server-side via webhooks (checkout.session.completed, or for subscriptions invoice.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 Conflict with:
{
  "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:

  • variant must be a Southbill variant id (uuid) belonging to your merchant account. Mixing variant and price in 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_id for subscriptions; variants are not supported in mode: "subscription".
  • amount and currency are derived from the variant price. If you send them and they do not match, the request fails with amount_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.