
# Errors

The API uses conventional HTTP status codes and returns a JSON body with an `error` field (and sometimes a `message`) on failure.

```json
{ "error": "business_not_approved", "message": "Your business must be approved …" }
```

## Status codes

| Status | Meaning | Typical causes |
|---|---|---|
| `200` | OK | Successful `GET`/`upload`. |
| `202` | Accepted | Verification queued (see [create verification](https://trust.myaza.co/documentation/api-create-verification/markdown)). |
| `400` | Bad request | Invalid body, bad/missing field, unsupported file type, file too large. |
| `401` | Unauthorized | Missing `Authorization` header, or invalid/revoked API key. |
| `403` | Forbidden | Production key without an approved business, a publishable key on a secret-only endpoint (`secret_key_required`), or a `requestId` owned by another org. |
| `404` | Not found | Verification ID does not exist (or isn't yours). |
| `429` | Too many requests | [Rate limit](https://trust.myaza.co/documentation/rate-limits/markdown) exceeded. |
| `500` | Server error | E.g. pricing not configured for the requested country/ID type. |

## Common error bodies

| `error` | Status | Meaning |
|---|---|---|
| `Missing or invalid Authorization header` | 401 | No Bearer token sent. |
| `Invalid API key` | 401 | Key is unknown or revoked. |
| `Invalid request body` | 400 | Verify payload failed validation; see `message` for details. |
| `Forbidden` | 403 | The `requestId` belongs to a different organisation. |
| `business_not_approved` | 403 | Production access requires an [approved business](https://trust.myaza.co/documentation/environments/markdown#production). |
| `secret_key_required` | 403 | A publishable (`pk_`) key was used on a secret-only endpoint (full result or media). Use a secret (`sk_`) key from your backend. See [authentication](https://trust.myaza.co/documentation/authentication/markdown). |
| `environment_mismatch` | 403 | The key's environment doesn't match the server it was sent to (e.g. a `pk_live_` key against the sandbox host). Use the base URL for the key's [environment](https://trust.myaza.co/documentation/environments/markdown). |
| `Verification not found` | 404 | Unknown verification ID. |
| `File too large (max 25MB)` | 400 | Upload exceeded the size cap. |
| `Too many requests, please try again later.` | 429 | Slow down; see [rate limits](https://trust.myaza.co/documentation/rate-limits/markdown). |
| `pricing_not_configured` | 500 | No price set for that country/ID type. Contact Myaza. |

## Asynchronous failures

A `202 Accepted` from [create verification](https://trust.myaza.co/documentation/api-create-verification/markdown) only means the request was queued. The verification can still finish as `failed`, `not_found`, or `error`. Those are **not** HTTP errors: read them from [`GET /status/:id`](https://trust.myaza.co/documentation/api-verification-status/markdown) or the [`verification.*` webhooks](https://trust.myaza.co/documentation/webhooks/markdown).

Each non-success outcome carries a human-readable `reason` plus a stable `reasonCode` you can branch on (e.g. `document_expired`, `selfie_mismatch`, `identity_not_found`). The full catalogue is in [failure reason codes](https://trust.myaza.co/documentation/verifications/markdown#failure-reason-codes).

## Health check

```
GET /api/kyc/health
```

A public, unauthenticated endpoint for uptime monitoring.

```json
{ "status": "ok" }
```

## Handling errors well

- Retry `429`, `502`, `503`, `504` and connection failures with backoff and the
  same idempotency key. Do not blindly retry every `500`: preserve its request ID
  and contact support if it persists.
- Treat `4xx` (except `429`) as **non-retryable**: fix the request; retrying unchanged will fail again.
- Always read the `error`/`message` fields; don't rely on status code alone.

## Server SDK errors (package preview)

The server SDK is not yet available on the public npm registry. For current
integrations, handle the HTTP status and error code as described above. When
the package is published, its typed errors will let your backend branch without
parsing messages:

```js
import {
  AuthenticationError,
  IdempotencyConflictError,
  InsufficientCreditError,
  MyazaError,
  RateLimitError,
  ValidationError,
} from '@myazahq/trust-sdk';

try {
  await myaza.transactions.retrieve('activity_123');
} catch (error) {
  if (error instanceof RateLimitError) {
    console.log('Retry after seconds:', error.retryAfterSeconds);
  } else if (error instanceof ValidationError || error instanceof IdempotencyConflictError) {
    console.error(error.code, error.details, error.requestId);
  } else if (error instanceof InsufficientCreditError) {
    console.log({ pending: error.pending, statusUrl: error.statusUrl });
  } else if (error instanceof AuthenticationError) {
    console.error('Check the key type and environment.');
  } else if (error instanceof MyazaError) {
    console.error(error.status, error.code, error.requestId);
  }
}
```

Transaction and activity decisions fail closed when credit is unavailable; a
late record is not an authorisation response. A paused customer risk assessment
sets `InsufficientCreditError.pending` or returns `pending_credit`, keeps the same
resource and resumes after funding. Do not submit a duplicate.
