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
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
{
"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
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
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);
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.