Errors
The most reliable signal: HTTP status
Regardless of which shape a failure comes back in (below), the HTTP status code is always meaningful and consistent:
| Status | Meaning |
|---|---|
401 |
Missing, malformed, or invalid API key |
403 |
Key is valid but lacks the required scope, or is subject-bound and refused |
404 |
Resource doesn't exist — or exists but belongs to a subject your key can't act for (these are indistinguishable on purpose, see Authentication & scopes) |
409 |
The request conflicts with existing state (e.g. a duplicate external reference) |
422 |
The request body failed validation |
429 |
Rate limit exceeded — check the Retry-After header |
503 |
A downstream provider is temporarily unavailable |
Build your retry/error-handling logic around the status code first; treat the body as supplementary detail.
Response body shapes you may see
KwiikPay's API has grown across two generations, and error bodies aren't fully unified yet. A robust client should be able to parse more than one shape:
1. The standard envelope (most common on /api/v1/partner validation/conflict errors):
{
"success": false,
"status_code": 422,
"message": "Invalid conversion pair.",
"data": { "code": "invalid_pair", "detail": "from_currency and to_currency are required and must be different." }
}
2. A flat error object (used for some not-found/auth cases on both API generations):
{ "title": "Customer was not found.", "detail": "No customer exists for the authenticated tenant and customer id.", "status": 404, "code": null }
3. RFC 7807 application/problem+json (written by shared middleware — authentication
failures, rate limiting, and idempotency-key errors all come back this way, on both generations):
{ "type": "...", "title": "Too Many Requests", "status": 429, "detail": "Rate limit exceeded for this route." }
Practical guidance
- Always branch on HTTP status first.
- For the message a user could see, try
data.detail(shape 1), thendetail(shapes 2 and 3), falling back totitleormessageif neither is present. - For a machine-readable reason code (to branch your own logic on), try
data.code(shape 1), thencode(shape 2) — not every error carries one; anull/missing code means "no specific machine code, use the status + message." - This is an active area of consolidation — a future API generation will converge on a single error shape (see the API Reference for what's currently documented per endpoint).