Errors
BetaOne shape for every refusal, a stable code to branch on, and an id to quote.
Every failure is an RFC 9457 problem,
sent as application/problem+json:
{
"type": "https://ghostsms.io/docs/api/errors/insufficient_balance",
"title": "Insufficient balance",
"status": 402,
"code": "insufficient_balance",
"detail": "This purchase needs $0.62 and the balance is $0.11.",
"request_id": "req_8f3a2c"
}
Branch on code, never on title or detail. The code is stable and part
of the contract; the sentences are written for people and will be improved.
Quote request_id to support. It identifies the one request in our logs, and
it is on every response — successful ones too — as the Request-Id header.
Some problems carry an extra field that tells you what to do next, such as
retry_after_seconds on a 429, required_scope on an insufficient scope, or
activation_id on a duplicate.
The codes you will actually handle
| Status | Code | What it means |
|---|---|---|
| 401 | authentication_required | No key, or not a key — a website token gets this too |
| 401 | api_key_revoked | The key was revoked, or the account's access was withdrawn |
| 403 | insufficient_scope | Real key, wrong scopes; carries required_scope |
| 403 | daily_spend_limit_reached | The account's daily spend ceiling; carries resets_at |
| 402 | insufficient_balance | Top up and retry with the same idempotency key |
| 409 | duplicate_pending_activation | You already have one open for this pair; carries activation_id |
| 409 | number_unavailable | Nothing free for that service and country right now |
| 409 | not_yet_available | Too early — no_code_available_at has not passed |
| 409 | request_conflict | The resource moved on: already cancelled, already refunded |
| 422 | validation_failed | A field is missing or malformed; the body names it |
| 429 | rate_limited | Carries Retry-After and retry_after_seconds |
404 is also how we answer a resource that belongs to somebody else — a key
never learns whether an id it cannot see exists.
Retrying
429 and 5xx are worth retrying, with backoff. 4xx otherwise is not: the
request needs changing, not repeating. When you do retry a purchase, send the
same idempotency key so the retry cannot buy twice.