# Payment links

Reusable hosted payment pages — one link, unlimited buyers.

# Payment links

A payment link is a reusable hosted payment page. Create it once, share the URL with as many buyers as you like — every buyer pays separately and each purchase becomes its own payment.

Links are created in the dashboard (**Payment links**) or over the API. Both paths share the exact same validation, limits and buyer-data rules.

Base URL: `https://api.southbill.com/v1`
Auth: `Authorization: Bearer sk_live_…` (or `sk_test_…` for sandbox links)

| Action | Scope |
| --- | --- |
| Read (`GET`) | `payments:read` |
| Write (`POST`, `PATCH`) | `checkout:write` |

Writes accept `Idempotency-Key`. Amounts are integers in minor units, currencies lowercase.

## The link address

Addresses are always generated by Southbill: `/i/` plus 9 random, case-sensitive characters, e.g.

```
https://payments.southbill.com/i/sNdnaxXi9
```

You cannot choose or change the code. Amount and currency are always resolved server-side from the stored link — never from the URL.

## Endpoints

| Method | Path | Description |
| --- | --- | --- |
| `GET` | `/v1/payment_links` | List links (`limit` <= 100, `starting_after`, `active`, `archived`) |
| `POST` | `/v1/payment_links` | Create a link |
| `GET` | `/v1/payment_links/:id` | Retrieve by id or code |
| `POST` or `PATCH` | `/v1/payment_links/:id` | Update a link |
| `POST` | `/v1/payment_links/:id/archive` | Archive (stops accepting buyers) |
| `POST` | `/v1/payment_links/:id/unarchive` | Restore an archived link |
| `GET` | `/v1/payment_links/:id/payments` | Purchases made through the link (`limit`, `status`) |

## Create

```bash
curl https://api.southbill.com/v1/payment_links \
  -H "Authorization: Bearer sk_live_…" \
  -H "Idempotency-Key: 4f1c9c0a-…" \
  -H "Content-Type: application/json" \
  -d '{
    "pricing_mode": "product",
    "merchant_price_id": "9f0c…",
    "headline": "Summer workshop ticket",
    "cta_label": "book_now",
    "quantity_mode": "customer",
    "max_quantity": 4,
    "max_uses": 50,
    "expires_at": "2026-12-31T23:59:59Z"
  }'
```

## Fields

**Pricing**

| Field | Type | Notes |
| --- | --- | --- |
| `pricing_mode` | `product` or `custom` | Default `product` |
| `merchant_price_id` (alias `price_id`) | string | Required for `product`. Must be an active, one-off price you own. Recurring prices need a subscription link. |
| `amount_cents` | integer | Required for `custom`. Minor units, above the currency minimum, max 100,000,000. |
| `currency` | string | Required for `custom` (lowercase ISO code). For `product` it is taken from the price. |
| `allow_custom_amount` | boolean | Buyer chooses the amount (donations, pay-what-you-want) |
| `min_amount_cents` / `max_amount_cents` | integer or null | Only with `allow_custom_amount` |

**Quantity**

| Field | Type | Notes |
| --- | --- | --- |
| `quantity_mode` | `fixed` or `customer` | Default `fixed` |
| `fixed_quantity` | integer | 1–10000, default 1 |
| `max_quantity` | integer | 1–10000, default 10 (used with `customer`) |

**Payment page**

| Field | Type | Notes |
| --- | --- | --- |
| `headline` | string, max 120 | |
| `description` | string, max 600 | |
| `custom_message` | string, max 300 | Extra note shown to the buyer |
| `reference` | string, max 80 | Internal reference, carried on the payment |
| `cta_label` | enum | `pay_now`, `pay_amount`, `buy_now`, `continue_to_payment`, `complete_purchase`, `book_now`, `donate_now`, `reserve_now` |
| `branding_mode` | `company_name` or `logo` | |
| `show_product_image` | boolean | Default `true` |
| `success_url` | https URL | Redirect after payment |

**Customer details**

Name, email, phone and billing address are **always** collected. They are the signals fraud screening needs, so a link created over the API can never be weaker than one created in the dashboard. Sending `collect_phone: false` or `collect_address: false` returns `400`.

| Field | Type | Notes |
| --- | --- | --- |
| `collect_shipping_address` | boolean | Ask for a separate delivery address |
| `collect_note` | boolean | Free-text field for the buyer |
| `note_label` | string, max 60 | Label for that field |

**Limits & state**

| Field | Type | Notes |
| --- | --- | --- |
| `max_uses` | integer or null | 1–1,000,000 or `null` for unlimited. Never accepted below the number of completed purchases. |
| `expires_at` | ISO-8601 or unix seconds, or null | Must be in the future |
| `active` | boolean | Pause/resume. Archived links must be unarchived first. |

## The payment_link object

```json
{
  "id": "7c2a…",
  "object": "payment_link",
  "livemode": true,
  "code": "sNdnaxXi9",
  "url": "https://payments.southbill.com/i/sNdnaxXi9",
  "active": true,
  "archived": false,
  "pricing_mode": "product",
  "merchant_price_id": "9f0c…",
  "product_name": "Summer workshop ticket",
  "amount": 12000,
  "currency": "eur",
  "allow_custom_amount": false,
  "quantity_mode": "customer",
  "fixed_quantity": 1,
  "max_quantity": 4,
  "cta_label": "book_now",
  "collect_phone": true,
  "collect_address": true,
  "max_uses": 50,
  "used_count": 3,
  "remaining_uses": 47,
  "expires_at": "2026-12-31T23:59:59Z",
  "view_count": 214,
  "stats": { "paid_count": 3, "open_count": 1, "volume": { "eur": 36000 } },
  "created": "2026-09-01T10:00:00Z"
}
```

## Purchases

```bash
curl "https://api.southbill.com/v1/payment_links/7c2a…/payments?status=completed" \
  -H "Authorization: Bearer sk_live_…"
```

Each entry is a `payment_link_payment` with `status`, `amount`, `currency`, `quantity`, `customer_name`, `customer_email`, `payment_intent`, `created` and `completed_at`.

Slots are reserved when a buyer starts checkout and released automatically when they abandon it, so the last seat is never sold twice. Retrieving a link reconciles those reservations first, so counters are always the truth.

## Sandbox

Use an `sk_test_…` key: links, prices, buyers and the payment page all run in the sandbox against test products and test cards, and come back with `livemode: false`. Test and live links are strictly separated — a test key never sees live links and vice versa. Test links are removed after 30 days or on a sandbox reset, and they do **not** appear in the dashboard (the dashboard shows live links only).

## SDKs

```js
const link = await southbill.paymentLinks.create({ pricing_mode: "custom", amount_cents: 2500, currency: "eur" });
await southbill.paymentLinks.list({ active: true });
await southbill.paymentLinks.payments(link.id);
await southbill.paymentLinks.archive(link.id);
```

```python
link = southbill.payment_links.create(pricing_mode="custom", amount_cents=2500, currency="eur")
southbill.payment_links.payments(link["id"])
southbill.payment_links.archive(link["id"])
```

Apps use the same object under `/v1/app/payment_links` with scopes `payments:read` / `payments:create`; the owner always comes from the installation token.

