Authentication
Bearer tokens, modes and installation context.
Base URL:
https://api.southbill.com/v1/app
Every request carries an installation access token:
curl "https://api.southbill.com/v1/app/payments?limit=10" \
-H "Authorization: Bearer sbat_live_…"
const res = await fetch("https://api.southbill.com/v1/app/payments?limit=10", {
headers: { Authorization: `Bearer ${accessToken}` },
});
if (!res.ok) throw new Error((await res.json()).error?.type ?? res.statusText);
const { data, has_more } = await res.json();
import requests
res = requests.get(
"https://api.southbill.com/v1/app/payments",
params={"limit": 10},
headers={"Authorization": f"Bearer {access_token}"},
timeout=30,
)
res.raise_for_status()
payments = res.json()["data"]
<?php
$ch = curl_init("https://api.southbill.com/v1/app/payments?limit=10");
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ["Authorization: Bearer {$accessToken}"],
]);
$body = json_decode(curl_exec($ch), true);
$payments = $body["data"];
The token identifies the app and the merchant. There is no merchant id parameter — you cannot address a merchant that has not installed you.
Getting a token
Tokens come from the OAuth flow at install time (authorization_code, PKCE supported) and are renewed with the refresh_token grant. See Installs & OAuth.
| Property | Value |
|---|---|
| Access token | sbat_live_… / sbat_test_…, valid 1 hour (expires_in: 3600) |
| Refresh token | sbrt_live_… / sbrt_test_…, single use and rotating — store the new one on every refresh |
| Mode | Baked into the token, cannot be switched |
| Scopes | Exactly what the merchant approved |
| Reissue | Issuing a new access token revokes the installation's previous access tokens |
Because refresh tokens rotate, refreshing twice with the same token invalidates the installation's tokens — serialise your refresh calls.
Failure modes
| Status | error.type |
Do this |
|---|---|---|
| 401 | invalid_token |
Refresh, then retry once |
| 403 | insufficient_scope |
Request the scope in a new version; do not retry |
| 403 | merchant_suspended |
Back off, retry later, keep data |
| 401 | installation_inactive |
Installation revoked or suspended — stop calling |
| 404 | not_found |
Object does not exist for this merchant |
| 404 | unknown_endpoint |
Path typo or unsupported resource |
| 405 | method_not_allowed |
Method not supported on this route. Reads are GET; Store writes use POST / PATCH / DELETE with the products:write, files:write or orders:write scope |
| 429 | rate_limit_exceeded |
Honour Retry-After |
Error bodies are uniform:
{ "error": { "type": "insufficient_scope", "message": "Missing required scope: payments:read" } }
Server-side only
The client secret and refresh tokens must never reach a browser or a mobile binary. Public clients use PKCE and keep only the short-lived access token.