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