# Subscriptions

Recurring billing on your Southbill account

# Subscriptions

Create recurring subscriptions for customers using a **recurring price** from your catalog. Subscriptions run on your connected Southbill account; Southbill deducts the platform fee automatically from each renewal invoice.

Base URL: `https://api.southbill.com/v1`
Auth: `Authorization: Bearer sk_live_…`

## Prerequisites

1. Create a **product** and a **recurring price** (see [Products](/docs/api/products) and [Prices](/docs/api/prices) — prices are created with `POST /v1/products/{product_id}/prices`; there is no top-level `/v1/prices` endpoint).
2. The price must have `recurring.interval` set (`day`, `week`, `month`, `year`).

## Create a subscription

`POST /v1/subscriptions`

```bash
curl -X POST https://api.southbill.com/v1/subscriptions \
  -H "Authorization: Bearer sk_live_…" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: sub_20260718_001" \
  -d '{
    "price_id": "price_1Tu…",
    "customer_name":  "Jane Doe",
    "customer_email": "jane@example.com",
    "quantity": 1,
    "trial_days": 14,
    "metadata": { "plan": "pro" }
  }'
```

### Body

| Field | Type | Required | Notes |
|---|---|---|---|
| `price_id` | string | ✅ | Recurring price from your catalog. |
| `customer` | string | ➖ | Existing Southbill customer (`cus_…`). Name and email are taken from that customer, and the same processor customer is reused. |
| `customer_name` | string | ✅* | 2–120 chars. *Not required when `customer` is set and has a name. |
| `customer_email` | string | ✅* | Valid email. *Not required when `customer` is set and has an email. |
| `quantity` | integer | ➖ | Defaults to `1`. |
| `trial_days` | integer | ➖ | Free trial before first charge. |
| `metadata` | object | ➖ | Free-form key/value returned in webhooks. |

### Response

```json
{
  "id": "sub_1Tu…",
  "object": "subscription",
  "status": "incomplete",
  "customer": "cus_1Tu…",
  "price_id": "price_1Tu…",
  "quantity": 1,
  "currency": "EUR",
  "amount": 1990,
  "current_period_start": 1735689600,
  "current_period_end":   1738368000,
  "latest_invoice": "in_1Tu…",
  "client_secret": "pi_1Tu…_secret_…",
  "trial_end": null,
  "created": 1735689600
}
```

`customer` in the response is your Southbill customer id when you passed one; otherwise it is the id created for this subscription. Processor ids are never returned here.

A `subscription.created` event is emitted immediately when the subscription is created, before the first payment.

### Completing an `incomplete` subscription

The subscription starts as `incomplete`. There is no `/v1/payment_intents` endpoint — confirm the returned `client_secret` in the browser:

```html
<script src="https://southbill.com/southbill.js"></script>
<script>
  const sb = southbill("pk_live_…");
  await sb.confirmPayment({
    client_secret: "pi_1Tu…_secret_…",
    return_url: "https://your-shop.com/thanks"
  });
</script>
```

`intent_type` tells you what the secret belongs to: `payment` (first invoice is charged now) or `setup` (trial — only the card is stored). After confirmation the status becomes `active` (or `trialing`) and `subscription.updated` plus the `invoice.*` events fire.

## Embedded (on-site) subscription checkout

The endpoint above returns a `client_secret` for a custom SDK confirmation. If you instead want Southbill's hosted card form rendered **inline on your page**, create the session through the Checkout Sessions endpoint with `mode: "subscription"`:

```bash
curl -X POST https://api.southbill.com/v1/checkout/sessions \
  -H "Authorization: Bearer sk_live_…" \
  -H "Content-Type: application/json" \
  -d '{
    "mode": "subscription",
    "line_items": [{ "price_id": "price_1Tu…" }],
    "customer_email": "jane@example.com",
    "success_url": "https://your-shop.com/thanks",
    "cancel_url":  "https://your-shop.com/cart"
  }'
```

The response returns `checkout_url`, `embed_url` **and** `client_secret` for the same session. To keep it on your site, mount `embed_url` in an iframe — or `southbill.checkout({ mode: 'embed', mount: '#southbill-checkout' })`. To redirect the buyer away, send them to `checkout_url`.

There is **no** `ui_mode` field — every session is always both hosted and embeddable. For subscriptions the behaviour is identical to one-time: redirect = hosted, `embed_url` = on-site.

Confirm the subscription server-side via the `invoice.paid` / `subscription.created` webhook rather than the browser event.

## Retrieve

```
GET /v1/subscriptions/{id}
```

## List

```
GET /v1/subscriptions?limit=20
```

Returns up to 100 subscriptions ordered by creation date (newest first).

## Cancel

`POST /v1/subscriptions/{id}/cancel`

```bash
curl -X POST https://api.southbill.com/v1/subscriptions/sub_1Tu…/cancel \
  -H "Authorization: Bearer sk_live_…" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: cancel_sub_1Tu_20260718" \
  -d '{ "at_period_end": true }'
```

| Field | Default | Meaning |
|---|---|---|
| `at_period_end` | `true` | Keep active until the current period ends, then cancel. |
| `at_period_end: false` | — | Cancel immediately. No further invoices. |

## Status model

| Status | Meaning |
|---|---|
| `incomplete` | Waiting for the first payment confirmation. |
| `incomplete_expired` | First payment was not confirmed within 23 h. |
| `trialing` | Free trial in progress. |
| `active` | Paid and current. |
| `past_due` | Renewal failed. Dunning in progress. |
| `unpaid` | All retries exhausted, subscription frozen. |
| `canceled` | Terminated. |

## Fees

Recurring invoices carry a platform fee computed from your plan and payment method. Fees are settled in **EUR** and deducted per invoice — same rules as one-off Checkout Sessions. See [Fees, currency & minimum amounts](/docs/getting-started/fees-and-currency).

## Idempotency

Send `Idempotency-Key: <your-key>` on `POST /v1/subscriptions` and `POST /v1/subscriptions/{id}/cancel`. Keys are scoped to `(merchant_id, method, path)` and expire after 24 hours.

- **Same key + same body** → original response replayed verbatim.
- **Same key + different body** → **`409 Conflict`** with `type: "idempotency_error"`, `code: "idempotency_key_reused"`. See [Error reference](/docs/api/errors).

## Errors

| HTTP | `error.type` | `error.code` | Meaning |
|---|---|---|---|
| 400 | `invalid_request` | field-specific | Missing / malformed field, or price is not recurring. |
| 401 | `authentication_error` | `invalid_api_key` | Bad, revoked or wrong-mode key. |
| 404 | `not_found` | `price_not_found` / `subscription_not_found` | Not owned by your account. |
| 409 | `idempotency_error` | `idempotency_key_reused` | Same key with different body. |
| 409 | `resource_conflict` | `merchant_not_ready` / `subscription_already_canceled` | See message. |
| 429 | `rate_limit_error` | `rate_limited` | Respect `Retry-After`. |

## Webhooks

Listen for these events (see [Event reference](/docs/webhooks/events) for payloads):

- `subscription.created` / `updated` / `deleted`
- `customer.subscription.trial_will_end` — fires 3 days before the trial ends
- `invoice.payment_succeeded` — renewal charged
- `invoice.payment_failed` — dunning stage advanced (notice → warning → final)

