Guides
Errors
Every failure looks the same on the wire. Branch on error.code, log error.message, ship error.details to your error reporter.
The error envelope
Failed responses share a single, stable shape. The HTTP status tells you the broad class; error.code tells you the specific reason; error.message is human-readable; error.details (when present) gives structured context.
json
{
"error": {
"code": "missing_fields",
"message": "Missing required field: orderId",
"details": {
"field": "orderId"
}
}
}Treat error.codeas the canonical signal. Messages may change wording over time without warning — codes won't.
Error code catalog
| Code | Status | What it means |
|---|---|---|
missing_token | 401 | No Authorization header was sent, or it didn't start with 'Bearer '. |
invalid_token | 401 | Token didn't match any active token — typo, wrong environment, or never minted in the first place. |
token_revoked | 401 | Token was revoked in the dashboard. Mint a new one; revocations are not reversible. |
token_expired | 401 | Token had an explicit expiry that's now in the past. Mint a replacement. |
insufficient_scope | 403 | Token is valid but doesn't carry the scope this endpoint requires. Check the scope listed in the API reference. |
ip_blocked | 403 | Request originated from an IP outside the token's allowlist. Check your egress IP, not your office WAN. |
rate_limited | 429 | Token exceeded its requests-per-minute budget. Honour the Retry-After header; see the rate limits guide. |
invalid_body | 400 | Request body wasn't valid JSON, or the Content-Type wasn't application/json. |
missing_fields | 400 | Body was JSON but a required field was missing or empty. The `details.field` value names which. |
refund_failed | 400 | Refund attempt rejected — too late, amount exceeds remaining refundable, or Paystack declined. See `details.reason`. |
internal_error | 500 | Something on our side broke. Safe to retry after a short pause; ping support if it persists. |
Handling errors in production
- Branch on
error.code. Switch statements overerror.messagewill break the day we improve the wording. - Retry only the retryable ones.
rate_limited,internal_error, and any network-level error are safe to retry with backoff. Don't retry 4xx codes other than 429 — they'll fail forever. - Surface
error.messageto humans. It's written for them. Shiperror.detailsto your error reporter for triage.
See the rate limits guide for the canonical backoff pattern, and the idempotency guide for retrying writes safely.