# 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**

```
{
  "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`, and `409 idempotency_request_is_still_processing` — always with the same `Idempotency-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-After` when present.

**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.
