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.