# 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_…` (or `sk_test_…`)

## Create a session

`POST /v1/checkout/sessions`

```bash
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

```json
{
  "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.

## Retrieve & expire

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

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:

```json
{
  "error": {
    "type": "idempotency_error",
    "code": "idempotency_key_reused",
    "message": "Idempotency-Key reused with a different request body"
  }
}
```

See [Error reference](/docs/api/errors) 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`. |

