# Hosted Checkout

Redirect to payments.southbill.com/c/{id} or embed payments.southbill.com/embed/{id} — card data never touches your servers, which minimizes your PCI scope (SAQ A).

# Hosted Checkout

Southbill ships two URLs for the hosted checkout — one for **real payments**, one for **design preview**. Do not mix them up.

| Route | Purpose | Real payment? |
|-------|---------|---------------|
| `/c/{session_id}` | Real hosted checkout for a session created via API | ✅ Yes |
| `/embed/{session_id}` | Same as above, optimised for iframe embedding | ✅ Yes |
| `/c/s/{slug}` | **Preview only** — renders your Checkout Builder branding with a dummy product. Ignores `?session=…`. | ❌ No |

## 1. Create a session (server-side)

```bash
curl -X POST https://api.southbill.com/v1/checkout/sessions \
  -H "Authorization: Bearer sk_live_…" \
  -H "Content-Type: application/json" \
  -d '{
    "amount": 4990,
    "currency": "CHF",
    "line_items": [{ "name": "T-Shirt", "quantity": 1, "amount": 4990 }],
    "success_url": "https://your-shop.com/thanks",
    "cancel_url":  "https://your-shop.com/cart"
  }'
```

Response:

```json
{
  "id": "cs_01H…",
  "checkout_url": "https://payments.southbill.com/c/cs_01H…?cs=cs_secret_…",
  "embed_url":    "https://payments.southbill.com/embed/cs_01H…"
}
```

## 2. Send the buyer to the real checkout

**Redirect**

```js
window.location = session.checkout_url;   // -> /c/{session_id}?cs={client_secret}
```

**Iframe**

```html
<iframe src="{{embed_url}}"
        style="width:100%;height:720px;border:0"
        allow="payment *"></iframe>
```

**Web component**

```html
<script src="https://payments.southbill.com/southbill.js"></script>
<southbill-checkout session-id="cs_01H…"></southbill-checkout>
```

The `slug` in the snippets from **Dashboard → Checkout builder** only tells Southbill *which branding profile* to apply — the `session_id` is what makes it a real transaction. Without a valid session, the page falls back to preview mode with dummy data.

## 3. What the buyer sees

- Merchant logo, business name and support email (from Checkout builder).
- Line items with product images (`line_items[].image_url`).
- All payment methods enabled for the currency in your plan + Stripe.
- 3-D Secure / SCA when required.
- Success page with southbill-hosted receipt (`/r/{merchant_id}/{tx_id}`) + PDF.

## Preview mode (`/c/s/{slug}`)

Use this URL in your browser to **see how your checkout looks** with the current Builder settings. It renders a hard-coded sample product for CHF 49.90 and a static list of payment methods. **Any `?session=…` parameter is ignored** — this route never talks to Stripe and never charges anyone.

To test a real payment end-to-end, always create a session via the API first and open the returned `checkout_url` **unchanged** — the `?cs=` query parameter is what authorises the browser to load that session.

## Success & cancel URLs

Optional. When set, Southbill redirects the buyer after the session ends and appends `?session_id={id}` so you can look up the result.

## Customising

**Dashboard → Checkout builder** controls colours, logo, business name, support email, return URL and the **pay button label**. Changes apply to every future session — no code changes needed.

### Pay button labels (fixed set)

The button text is **not free text**. It is picked from a curated list in **Dashboard → Checkout builder → Button label**, so the button can never say something misleading about what the buyer is agreeing to. There is no API field for it — the label always comes from the Builder profile of the merchant that owns the session.

| Value stored | Renders as | Allowed for |
|---|---|---|
| `Pay {amount}` | `Pay $99.00` | one-time (`mode: "payment"`) |
| `Pay securely` | `Pay securely` | one-time |
| `Complete purchase` | `Complete purchase` | one-time |
| `Buy now` | `Buy now` | one-time |
| `Place order` | `Place order` | one-time |
| `Donate {amount}` | `Donate $99.00` | one-time |
| `Subscribe` | `Subscribe` | subscription (`mode: "subscription"`) |
| `Subscribe now` | `Subscribe now` | subscription |
| `Start subscription` | `Start subscription` | subscription |
| `Pay subscription` | `Pay subscription` | subscription |

Rules:

- `{amount}` is replaced at render time with the formatted session total (currency + locale). It is the only placeholder.
- A one-time label is never shown on a subscription checkout. If the profile holds a one-time label and the session is `mode: "subscription"`, the checkout falls back to **Start subscription**.
- Defaults: `Pay {amount}` for one-time, `Start subscription` for subscriptions.
- Legacy or unknown values (from before the fixed list) fall back to the defaults instead of rendering raw text.
- Any other value sent to the Checkout Builder API is rejected with `400 invalid_request`.

### Wallets (Apple Pay / Google Pay)

Express wallet buttons are rendered above the card form by Stripe's Express Checkout Element. They appear only when **all** of the following are true:

- wallets work for one-time **and** subscription sessions. For `mode: "subscription"` the wallet sheet shows the recurring terms (amount, interval, cancellation link) and stores the mandate for future billing cycles;
- express checkout is enabled in the Checkout Builder and the wallet is in the enabled wallet list;
- the buyer's browser/device actually offers the wallet (Apple Pay: Safari/iOS with a card in Wallet; Google Pay: Chrome with a saved card);
- the page is served over HTTPS.

Nothing has to be configured on your own domain — the wallet runs inside the Southbill checkout origin (`payments.southbill.com`), which is already registered with Apple. When you embed the checkout, keep `allow="payment *"` on the iframe, otherwise the browser blocks the wallet inside the frame.


## Ad-hoc vs. catalog line items

| Key type | Allowed `line_items` |
| --- | --- |
| Secret key (`sk_…`, server-side) | Ad-hoc items with `name`, `quantity`, `amount`, or catalog items with `price`. |
| Publishable key (`pk_…`, browser) | **Only** catalog items: `{ "price": "price_…", "quantity": 1 }`. Amounts are resolved server-side from your catalog. |

