Invoices
Create, send, void and reconcile invoices over the API — or from the dashboard.
Invoices
Invoices can be created in the dashboard (/dashboard/invoices/new) or over the API. Every invoice produces a hosted payment page at https://payments.southbill.com/i/{token} that anyone with the link can pay — no key needed on the customer side.
Base URL https://api.southbill.com · Auth Authorization: Bearer sk_live_… · Scopes invoices:read, invoices:write.
Endpoints
| Method | Path | Scope | Purpose |
|---|---|---|---|
POST |
/v1/invoices |
invoices:write |
Create a draft (or send immediately with auto_send). |
GET |
/v1/invoices |
invoices:read |
List invoices — ?status=, ?customer=, ?limit=, ?starting_after=. |
GET |
/v1/invoices/{id} |
invoices:read |
Retrieve one invoice incl. lines. |
POST |
/v1/invoices/{id} |
invoices:write |
Update a draft invoice. |
POST |
/v1/invoices/{id}/send |
invoices:write |
Finalize → open, mint the public token, email the customer. |
POST |
/v1/invoices/{id}/void |
invoices:write |
Void an unpaid invoice. |
POST |
/v1/invoices/{id}/mark_paid |
invoices:write |
Record an out-of-band payment (bank transfer, cash). |
GET |
/v1/invoices/{id}/installments |
invoices:read |
Instalment plan of a partial invoice, ordered by sequence. |
GET |
/v1/invoices/{id}/payments |
invoices:read |
Every payment booked against the invoice, oldest first. |
Create and send
curl https://api.southbill.com/v1/invoices \
-H "Authorization: Bearer $SOUTHBILL_SECRET_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: inv_9781" \
-d '{
"customer": "cus_9f2c41a0b7e34d9a8c15be22",
"currency": "eur",
"due_date": "2026-09-30",
"memo": "Thanks for your business.",
"auto_send": true,
"line_items": [
{ "description": "Implementation, September", "quantity": 1, "unit_amount": 120000, "tax_rate": 20 },
{ "description": "Support retainer", "quantity": 3, "unit_amount": 15000, "tax_rate": 20 }
]
}'
const invoice = await fetch("https://api.southbill.com/v1/invoices", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.SOUTHBILL_SECRET_KEY}`,
"Content-Type": "application/json",
"Idempotency-Key": "inv_9781",
},
body: JSON.stringify({
customer: "cus_9f2c41a0b7e34d9a8c15be22",
currency: "eur",
due_date: "2026-09-30",
auto_send: true,
line_items: [
{ description: "Implementation, September", quantity: 1, unit_amount: 120000, tax_rate: 20 },
],
}),
}).then((r) => r.json());
console.log(invoice.hosted_invoice_url);
import os, requests
invoice = requests.post(
"https://api.southbill.com/v1/invoices",
headers={
"Authorization": f"Bearer {os.environ['SOUTHBILL_SECRET_KEY']}",
"Idempotency-Key": "inv_9781",
},
json={
"customer": "cus_9f2c41a0b7e34d9a8c15be22",
"currency": "eur",
"auto_send": True,
"line_items": [
{"description": "Implementation, September", "quantity": 1, "unit_amount": 120000, "tax_rate": 20}
],
},
timeout=30,
).json()
print(invoice["hosted_invoice_url"])
<?php
$ch = curl_init("https://api.southbill.com/v1/invoices");
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => [
"Authorization: Bearer " . getenv("SOUTHBILL_SECRET_KEY"),
"Content-Type: application/json",
"Idempotency-Key: inv_9781",
],
CURLOPT_POSTFIELDS => json_encode([
"customer" => "cus_9f2c41a0b7e34d9a8c15be22",
"currency" => "eur",
"auto_send" => true,
"line_items" => [[
"description" => "Implementation, September",
"quantity" => 1,
"unit_amount" => 120000,
"tax_rate" => 20,
]],
]),
]);
$invoice = json_decode(curl_exec($ch), true);
Response:
{
"id": "inv_01J…",
"object": "invoice",
"livemode": true,
"number": "SPK-INV-2026-0184",
"status": "open",
"currency": "eur",
"subtotal": 165000,
"tax": 33000,
"total": 198000,
"customer": "cus_9f2c41a0b7e34d9a8c15be22",
"customer_email": "jane@example.com",
"due_date": "2026-09-30",
"hosted_invoice_url": "https://payments.southbill.com/i/9f14c0…",
"paid_at": null,
"created": 1756000000,
"lines": {
"object": "list",
"data": [
{
"id": "li_0",
"object": "invoice_item",
"description": "Implementation, September",
"quantity": 1,
"unit_amount": 120000,
"tax_rate": 20,
"amount": 120000
}
]
}
}
Request fields
| Field | Required | Notes |
|---|---|---|
line_items[] |
yes | 1–200 items. Each needs description and an integer unit_amount in minor units; quantity > 0, tax_rate 0–100. |
currency |
yes | ISO 4217, lowercase (eur, gbp, usd). |
customer |
yes* | Customer id. Or pass customer_email directly. |
customer_email, customer_name, customer_company, customer_tax_id, customer_address |
no | Override the values copied from the customer. |
due_date |
no | YYYY-MM-DD. |
memo, notes, metadata |
no | Free-form; returned on every read and webhook. |
auto_send |
no | true finalizes and emails in the same call. |
payment_mode |
no | full (default), partial (instalment plan) or open (customer chooses the amount). |
installment_count |
for partial |
2–6 instalments. Each instalment must stay above the currency minimum. |
installment_interval |
for partial |
weekly, biweekly, monthly or custom. |
installment_interval_days |
for custom |
1–365 days between instalments. |
first_installment_due_date |
for partial |
YYYY-MM-DD, due date of instalment 1. |
installment_reminders_enabled |
no | Default true: emails the customer 3 days before and on each due date. |
min_payment_amount, max_payment_amount |
no | Bounds per payment in open mode, minor units. |
allow_overpayment |
no | open mode only: accept more than the outstanding amount. |
Partial payments
A partial invoice is settled by several payments. Southbill keeps amount_paid and amount_due
on the invoice, tracks each instalment separately and moves the invoice through
open → partially_paid → paid. Read the plan with GET /v1/invoices/{id}/installments:
{
"object": "list",
"has_more": false,
"data": [
{ "id": "…", "sequence": 1, "amount": 66000, "amount_paid": 66000, "currency": "eur", "due_date": "2026-10-01", "status": "paid" },
{ "id": "…", "sequence": 2, "amount": 66000, "amount_paid": 0, "currency": "eur", "due_date": "2026-11-01", "status": "open" }
]
}
GET /v1/invoices/{id}/payments returns each booked payment (source: "stripe" for online
payments, source: "manual" for payments recorded out of band) with amount, amount_refunded,
status and paid_at. A refund on a paid partial invoice moves it back to partially_paid.
Amounts are integers in the smallest currency unit — 120000 = 1 200.00 EUR. Totals are computed server-side from the line items; you cannot set total directly.
Lifecycle
draft ──send──► open ──pays in full──────────────► paid
│ │
│ ├──pays part (partial/open)──► partially_paid ──rest paid──► paid
│ ├──mark_paid──────────────────► paid
└──void────────┴──void────────────────────────► void
A refund on a paid invoice with an outstanding balance returns it to partially_paid.
| Rule | Behaviour |
|---|---|
| Update | Only draft invoices — otherwise 400 invoice_not_draft. |
| Void | Never on a paid invoice (400 invoice_paid); blocked while a payment is in flight (409 invoice_payment_in_progress). |
mark_paid |
Idempotent — a paid invoice returns the invoice unchanged; a void invoice returns 400 invoice_void. |
| Numbering | SPK-INV-YYYY-NNNN, with a separate sequence per mode so test traffic never consumes live numbers. |
| Minimum | Total must be ≥ 2.50 in the invoice currency. |
Webhooks
| Event | When |
|---|---|
invoice.created |
Created over the API. |
invoice.sent |
Finalized and emailed to the customer. |
invoice.finalized |
draft → open. |
invoice.paid |
Paid in full by the customer or via mark_paid. |
invoice.payment_succeeded |
A single payment was booked — carries amount_received. Fires for every payment, including the last one. |
invoice.partially_paid |
The invoice moved to partially_paid (part of the total is settled). |
invoice.installment.paid |
An instalment of a partial invoice is fully settled. |
invoice.installment.due |
An instalment is due today. |
invoice.installment.overdue |
An instalment passed its due date unpaid. |
invoice.payment_failed |
Payment attempt failed. |
invoice.voided |
Voided. |
Every one of these is also stored in the Events log, so you can replay after downtime.
Rate limits: 300 reads/min and 60 writes/min per key. All writes accept Idempotency-Key.