Core concepts
Errors
Errors use HTTP status codes and a stable, snake_case error.code. Branch on the code. error.details is optional and code-specific.
422 Unprocessable Entity
422 Unprocessable Entity
{
"status": "fail",
"data": null,
"error": {
"code": "validation_error",
"message": "Validation failed",
"details": {
"fields": {
"rules.3.price": ["price must be a decimal string with at most 2 decimals"],
"reason": ["reason must be at least 3 characters"]
}
}
},
"metadata": { "requestId": "8d7e2c1a-41f0-4a4e-9a55-6f0b3c2d1e77", "…": "…" }
}Generic codes#
Any endpoint can return these. Business codes (package_incomplete, claim_not_payable, …) are listed on each endpoint in the reference.
| Status | Code | Meaning | Do |
|---|---|---|---|
| 400 | bad_request | Malformed JSON, missing or unknown field, or an action not allowed in the resource's current status. | Fix the request. Don't retry as is. |
| 400 | invalid_scope | Token request for a scope the client doesn't hold. | Request only granted scopes. |
| 401 | invalid_client | Wrong client credentials, or client revoked. | Check credentials; ask your account manager. |
| 401 | unauthorized | Token missing, invalid or expired. | Get a new token and retry once. |
| 403 | missing_permission | The token lacks the endpoint's scope. | Ask for the scope on the client. |
| 403 | forbidden | The client can never do this. | Don't retry. |
| 403 | ip_not_allowed | IP not on the client's allow-list. | Fix the allow-list or call from an allowed IP. |
| 403 | tenant_not_active | Your company is suspended on Wathiqaty. | Contact your account manager. |
| 404 | not_found | Doesn't exist, or isn't your company's. details.resource names it. | Treat as missing. |
| 408 | request_timeout | The request took too long. | Retry with the same Idempotency-Key. |
| 409 | conflict | Conflicts with current state, e.g. a duplicate reference. | Reload, then decide. |
| 409 | idempotency_request_is_still_processing | Same key still running. | Wait 1 s, retry with the same key. |
| 412 | version_conflict | Changed since you read it. | Reload, re-apply, resend with the new If-Match. |
| 413 | payload_too_large | Body over 100 KB. | Send less. |
| 422 | validation_error | Field values invalid. details.fields maps each path to messages. | Fix the values. |
| 422 | idempotency_key_already_used | Same key with a different request. | New key for a new action. |
| 429 | rate_limited | Too many requests. | Wait Retry-After seconds. |
| 500 | internal_server_error | Unexpected error on our side. | Retry with backoff; report the request id if it persists. |
| 503 | service_unavailable | A dependency is down or we are deploying. | Retry with backoff, honour Retry-After. |
Retrying#
- Retry: network errors,
408,429,5xx, and409 idempotency_request_is_still_processing— always with the sameIdempotency-Key. - Refresh and retry once:
401 unauthorized. - Never retry unchanged: any other
4xx. - Back off exponentially with jitter (1 s, 2 s, 4 s … up to 60 s), and honour
Retry-Afterwhen present.
Pseudocode
Pseudocode
async function call(request, attempt = 0) {
const res = await send(request) // same Idempotency-Key on every attempt
if (res.ok) return res
const retryable = [408, 429, 500, 502, 503, 504].includes(res.status)
|| (res.status === 409 && res.code === "idempotency_request_is_still_processing")
if (res.status === 401 && attempt === 0) {
await refreshToken()
return call(request, attempt + 1)
}
if (!retryable || attempt >= 6) throw res
const wait = res.retryAfter ?? Math.min(60, 2 ** attempt) * (0.5 + Math.random())
await sleep(wait * 1000)
return call(request, attempt + 1)
}Status not allowed
Moving a resource to a status its current status doesn't allow (e.g. approving a claim that's still submitted) is 400 bad_request with details.from and details.to. Reload the resource: someone (your staff, the customer, or the system) moved it first.