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 and 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

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

{
  "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:

<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":

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

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.

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.

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 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)