Authentication, scopes, rate limits and the core endpoints.
Everything the App API exposes today.
The App API is how your app reads the merchant's data. Paths below are relative to the base URL — /payments means https://api.southbill.com/v1/app/payments. Version 1 is read-only: every endpoint is a GET, anything else returns 405 method_not_allowed. Write scopes exist in the scope reference but are not callable in v1; they are granted case by case after review.
Base URL: https://api.southbill.com/v1/app · Auth: Authorization: Bearer sb_at_live_… (see Authentication).
Endpoints
| Method |
Path |
Scope |
What it gives you |
| GET |
/merchant |
merchant:read |
Profile of the merchant that installed your app: company, country, VAT number, status. Call once after install to label the connection. |
| GET |
/payments |
payments:read |
Processed payments, newest first. Live traffic supports ?status=succeeded. |
| GET |
/payments/{id} |
payments:read |
One payment. The {id} is the charge_id in live and the row id in sandbox. |
| GET |
/refunds |
refunds:read |
Sandbox: refund rows. Live: refunded charges with the running amount_refunded. |
| GET |
/disputes |
disputes:read |
Chargebacks including evidence_due_by and past_due. |
| GET |
/payouts |
payouts:read |
Bank settlements. Restricted scope — reconciliation tools only. |
| GET |
/balance |
payouts:read |
Available and pending balance of the merchant. |
| GET |
/products |
products:read |
The merchant's catalog (no prices in v1). |
| GET |
/products/{id} |
products:read |
One product. |
| GET |
/invoices |
invoices:read |
Issued invoices. Live only — sandbox returns an empty list. |
| GET |
/invoices/{id} |
invoices:read |
One invoice. |
| GET |
/subscriptions |
subscriptions:read |
Recurring agreements with status and period end. |
| GET |
/analytics/summary |
analytics:read |
Aggregated volume for a window: ?days=30 (1–365). |
Field-by-field descriptions of every response are in Objects & fields.
Query parameters
| Parameter |
Where |
Behaviour |
limit |
all list endpoints |
1–100, default 25. |
offset |
all list endpoints |
Row offset, default 0. |
status |
/payments (live) |
Exact match, e.g. ?status=succeeded. |
days |
/analytics/summary |
1–365, default 30. |
Unknown query parameters are ignored, not rejected. There are no date-range filters in v1 — page by offset and stop at the first object older than your cursor.
Pagination
curl "https://api.southbill.com/v1/app/payments?limit=50&offset=100" \
-H "Authorization: Bearer sb_at_live_…"
{ "object": "list", "data": [ … ], "has_more": true }
Lists are sorted newest first and carry no total count. Iterate until has_more is false. New objects arrive while you page, so for exports read the first page frequently rather than deep-paging a moving list.
Test vs live
The token decides the data source: a sandbox token reads your environment's simulated data, a live token reads the merchant's real data. Field names differ between the two — see the table at the top of Objects & fields.
Amounts and timestamps
Amounts are integer minor units with a lowercase ISO currency: { "amount": 2500, "currency": "eur" } is €25.00. Never use floats. Timestamps are ISO-8601 UTC; arrival_date, issue_date and due_date are dates without a time part.
Errors
{ "error": { "type": "insufficient_scope", "message": "Missing required scope: payments:read" } }
See Error & status codes.
The shape of every resource you can read.
This page describes the actual JSON the App API returns, field by field.
Sandbox and live return the same object shape. The only difference is the data source and the livemode flag — you never need two parsers.
Global rules
| Rule |
What it means |
| Amounts are integers in minor units |
2500 with "currency": "eur" is €25.00. Never use floats. |
| Currencies are lowercase ISO-4217 |
eur, chf, usd. |
| Timestamps are unix seconds |
created, arrival_date, due_date, current_period_end are integers (UTC seconds), or null. |
livemode tells you the source |
false = your sandbox environment, true = the installed merchant's real data. |
| IDs are opaque |
Sandbox uses UUIDs, live uses processor IDs (ch_…, dp_…, po_…). Never parse them. |
| Objects are additive |
Ignore unknown fields instead of failing. |
object names the type |
payment, refund, dispute, payout, product, invoice, subscription, merchant, balance, analytics_summary, list. |
Fields that do not exist in one environment are null |
e.g. fee / net are sandbox-only, past_due is live-only. |
Lists are { "object": "list", "data": [...], "has_more": bool } and accept ?limit= (max 100) and ?offset=.
Merchant — GET /merchant
{ "object": "merchant", "id": "…", "business_name": "Ada GmbH", "email": "ops@ada.de",
"country": "DE", "city": "Berlin", "vat_number": "DE123456789", "reference": "M-10423",
"status": "approved", "plan_id": "…", "livemode": true }
| Field |
Meaning |
business_name |
Legal/company name from onboarding. |
status |
Onboarding state — only approved merchants process live payments. |
plan_id |
The merchant's Southbill plan; null in sandbox. |
Payment — GET /payments, GET /payments/{id}
{ "object": "payment", "livemode": true, "id": "ch_3Nk91x", "payment_intent": "pi_3Nk91x",
"charge": "ch_3Nk91x", "amount": 2500, "amount_refunded": 0, "fee": null, "net": null,
"currency": "eur", "status": "succeeded", "refunded": false, "disputed": null,
"description": "Order #1042", "customer_email": "ada@example.com", "customer_name": "Ada Lovelace",
"payment_method_type": "card", "card_brand": "visa", "card_last4": "4242", "card_country": "DE",
"receipt_url": "https://…", "failure_code": null, "failure_message": null,
"created": 1755423672 }
| Field |
Meaning |
id |
Use it for GET /payments/{id}. Live: the charge id. Sandbox: the row id. |
amount / amount_refunded |
Minor units. amount - amount_refunded is what the merchant kept. |
fee / net |
Simulated Southbill fee and net amount — sandbox only, null in live. |
status |
succeeded means paid. Also pending, failed, canceled. |
refunded |
true once fully refunded; a non-zero amount_refunded with false is a partial refund. |
created |
Unix seconds; also the sort key of the list. |
Filter live payments with ?status=succeeded.
Refund — GET /refunds
{ "object": "refund", "livemode": true, "id": "ch_3Nk91x_refund", "charge": "ch_3Nk91x",
"payment_intent": "pi_3Nk91x", "amount": 500, "currency": "eur",
"status": "succeeded", "reason": null, "created": 1755500530 }
In live, one row represents the running refunded total of a charge (reason is null). In sandbox, each refund row is returned individually including its reason.
Dispute — GET /disputes
{ "object": "dispute", "livemode": true, "id": "dp_7c", "charge": "ch_3Nk91x",
"payment_intent": null, "amount": 2500, "fee": null, "currency": "eur",
"status": "needs_response", "reason": "fraudulent", "evidence_due_by": 1756684799,
"evidence_submitted_at": null, "past_due": false, "closed_at": null, "created": 1755602100 }
| Field |
Meaning |
status |
needs_response → under_review → won or lost. |
evidence_due_by |
Hard deadline in unix seconds. |
past_due |
Live only — null in sandbox. |
Payout — GET /payouts
{ "object": "payout", "livemode": true, "id": "po_1M2", "amount": 128400, "currency": "eur",
"status": "paid", "method": "standard", "destination_bank_name": "N26",
"destination_last4": "4321", "arrival_date": 1755561600,
"failure_code": null, "failure_message": null, "created": 1755561600 }
Product — GET /products, GET /products/{id}
{ "object": "product", "livemode": true, "id": "…", "name": "T-Shirt", "description": "…",
"active": true, "images": [], "tags": [], "unit_label": null, "url": null,
"created": 1750000000, "updated": 1755000000 }
Prices are not exposed in App API v1.
Invoice — GET /invoices, GET /invoices/{id}
{ "object": "invoice", "livemode": true, "id": "…", "invoice_number": "SPK-INV-2026-0042",
"status": "paid", "currency": "eur", "subtotal_amount": 10000, "tax_amount": 1900,
"total_amount": 11900, "customer_name": "Ada GmbH", "customer_email": "ada@example.com",
"customer_type": "business", "issue_date": 1755000000, "due_date": 1757000000,
"paid_at": 1755600000, "created": 1755000000 }
Live only. In sandbox, the list is empty and GET /invoices/{id} returns 404.
Subscription — GET /subscriptions
{ "object": "subscription", "livemode": true, "id": "…", "status": "active",
"currency": "eur", "amount": 1990, "quantity": 1,
"customer_email": "ada@example.com", "customer_name": "Ada Lovelace",
"current_period_start": 1755000000, "current_period_end": 1757592000,
"cancel_at_period_end": false, "created": 1750000000 }
Balance — GET /balance
{ "object": "balance", "livemode": true, "available": [{ "amount": 128400, "currency": "eur" }],
"pending": [{ "amount": 25000, "currency": "eur" }] }
Analytics — GET /analytics/summary?days=30
{ "object": "analytics_summary", "livemode": true, "days": 30, "currency": "eur",
"gross_amount": 1284000, "net_amount": 1240000, "payment_count": 412,
"refund_amount": 12000, "refund_count": 3 }