> ## Documentation Index
> Fetch the complete documentation index at: https://docs.sahlfinancial.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Errors

> Every error body the Partner API returns, with status, cause and fix.

## Error shapes

There are three shapes. Read the status first, then the body.

**1. `detail` is a string** (most errors).

```json theme={null}
{ "detail": "Invalid or revoked API key" }
```

**2. `detail` is an object with a stable `code`** (match on `code`, not on the message).

```json theme={null}
{
  "detail": {
    "code": "kyc_extract_cap_reached",
    "message": "Monthly document-read limit of 2000 reached.",
    "used": 2000,
    "limit": 2000
  }
}
```

**3. `code` and `message` at the top level, with no `detail`.** These come from the rate limiter, the unknown-route handler and the catch-all for unexpected errors.

```json theme={null}
{ "code": "rate_limit_exceeded", "message": "Too many requests. Please slow down." }
```

A 422 schema error has a list in `detail`, one entry per bad field:

```json theme={null}
{
  "detail": [
    { "type": "string_pattern_mismatch", "loc": ["body", "reference"], "msg": "String should match pattern '^[A-Za-z0-9_.:-]+$'", "input": "client 1", "ctx": { "pattern": "^[A-Za-z0-9_.:-]+$" } }
  ]
}
```

`loc` says where: `["body", "reference"]` for a JSON body, `["query", "environment"]` or `["path", "key"]`. For a multipart form field the first item is `body` as well. Handle all three shapes: a client that assumes `detail` is always an object will fail on the first 422.

## Request id

Every response carries `X-Request-ID`. If you send your own (1 to 64 characters of `A-Z a-z 0-9 . _ : -`) Sahl returns it; otherwise Sahl creates one. Each key call is listed in **Developers, Call log** in the console under that id. Quote it when you write to Sahl.

## Catalogue

### 400 Bad request

| Message | Cause | Fix |
| - | - | - |
| `Send between 1 and 5 files.` | `/extract` got no file or more than 5. | Send 1 to 5 `files` parts. |
| `Unsupported file type '<mime>'. Allowed types: application/pdf, image/jpeg, image/png, image/tiff, image/webp` | The part's `Content-Type` is not one of the five. `image/jpg` is accepted as JPEG. | Convert the file or set the right content type on the part. |
| `File content does not match declared MIME type.` | The first bytes do not match the content type you declared (a PNG sent as `application/pdf`, or a file that is not an image or PDF). | Send the real file with its real type. A PDF may have up to 1 KB before `%PDF-`. |

### 401 Unauthorized

| Message | Cause | Fix |
| - | - | - |
| `Missing API key` | No `Authorization: Bearer ...` header. | Send it. |
| `Invalid or revoked API key` | Wrong, malformed or revoked key. | Check the key. Create a new one if it was revoked. |
| `API key expired` | A rotated key whose grace period ended. | Use the new key. |
| `Partner identity could not be verified` | The key is bound to a service account and `X-Partner-Identity` is missing or wrong. | Send a valid Google ID token for the bound account and the audience in the key form. |

### 403 Forbidden

| Body | Cause | Fix |
| - | - | - |
| `"API key lacks the kyc:verify scope"` (string, scope name varies) | The key does not hold the scope for the endpoint. | Create a key with the scope. See [scopes](/authentication#scopes). |
| `{"code": "kyc_scope_not_allowed", "message": "This workspace is not enabled for the partner KYC API."}` | The key has a `kyc:` scope but the workspace is not enabled. Partner API access is per workspace. | [Request sandbox access](https://sahlfinancial.com/contact?type=demo). |
| `{"code": "direct_access_refused", "message": "Call the API at https://app.sahlfinancial.com/api."}` | You called Sahl's internal address. | Use the public host. |

### 404 Not found

| Body | Cause | Fix |
| - | - | - |
| `{"detail": "identity verification is not set up for this tenant"}` | An eID endpoint on a workspace with no eID provider account. | [Contact Sahl](https://sahlfinancial.com/contact?type=demo) to have eID set up. |
| `{"detail": "Not Found"}` | `GET /v1/kyc/eid/{key}` or its report: the key does not exist, or belongs to another workspace. | Use the `key` your `POST /v1/kyc/eid` returned. |
| `{"code": "not_found", "message": "API endpoint not found: GET /api/..."}` | The path or method does not exist. | Check the path. The base URL ends in `/api`. |

### 413 Payload too large

| Message | Cause | Fix |
| - | - | - |
| `File too large. Maximum allowed size is 30 MB.` | A file is over the limit (30 MB by default). | Compress or split. |
| `Image is 9000x9000 pixels; the maximum is 50 megapixels.` | The image header declares more than 50 megapixels. A small file can still be refused. | Downscale. |
| `Image dimensions are too large to process.` | Pillow flagged a decompression bomb. | Send a normal image. |

### 422 Unprocessable

| Message | Cause | Fix |
| - | - | - |
| `reference must be 1-64 characters of A-Z a-z 0-9 _ . : -` | `/extract` form field. | Use only those characters. |
| `environment must be 'sandbox' or 'production'` | `/extract` form field. | Use one of the two. |
| `subject must be at most 255 characters` | `/extract` form field. | Shorten it. |
| `identity verification is available for Canadian clients only` | `/v1/kyc/eid` with a `country` that is not `CA` or `CAN`. | eID is for Canadian clients. |
| A list of `{type, loc, msg}` | JSON, query or path field fails the schema: a bad `reference` or `environment`, `email` not an email, `documents` not 1 or 2, `key` not an integer, a missing required field. | Read `loc` and `msg`. |

A bad `kind` is not an error: unknown values count as `individual`.

### 429 Too many requests

| Body | Cause | Fix |
| - | - | - |
| `{"detail": {"code": "kyc_extract_cap_reached", "message": "Monthly document-read limit of 2000 reached.", "used": 2000, "limit": 2000}}` | The workspace used its reads for the calendar month (UTC). Counted before the model runs, so a refused call spends nothing. | Wait for the next month, or ask Sahl to raise it. Do not retry. |
| `{"code": "rate_limit_exceeded", "message": "Too many requests. Please slow down."}` | More than 100 requests in a minute from one client IP on these routes. Headers: `Retry-After` (seconds), `X-RateLimit-Limit`, `X-RateLimit-Remaining`. | Wait `Retry-After` seconds. |

Every successful response also carries `X-RateLimit-Limit` and `X-RateLimit-Remaining`. The limit is per client IP, not per key, so several servers behind one address share it. 100 a minute is the default in the code and can change.

### 500 and 502

| Status | Body | Cause | Fix |
| - | - | - | - |
| 500 | `{"code": "internal_error", "message": "An unexpected error occurred", "details": null}` | An unexpected failure on Sahl's side. | Retry once. If it persists, quote `X-Request-ID`. |
| 502 | `{"detail": "the identity verification service did not answer"}` | The eID provider did not answer. | Retry later. |

A reader that cannot be reached is not an error: `/extract` answers 200 with `reader_unavailable: true`. See [Read documents](/guides/ocr-documents#reader_unavailable).

## Retries

The API has no idempotency key. Think about each call before you retry it.

| Call | Safe to retry? | What a retry does |
| - | - | - |
| `GET /v1/kyc/eid/{key}` and `/report` | Yes | Reads again. |
| `POST /v1/kyc/verify`, `POST /v1/kyc/assess` | Yes in effect | The verdict is a pure function of the body. A retry with a `reference` files the verdict on the case again and sends the webhook again. De-duplicate on `event_id`. |
| `POST /v1/kyc/extract` | Only after a network failure or a 5xx | Reads again and counts again. With a `reference` it files the documents again, so the case then holds two copies. |
| `POST /v1/kyc/eid` | Avoid | Emails the client again and replaces the request recorded on the case. |

Rules of thumb:

* Never retry a 4xx, except a 429 for `rate_limit_exceeded`, which has `Retry-After`. A 4xx will fail the same way.
* Retry a 5xx and a network timeout with exponential backoff and a cap, for example 2 s, 4 s, 8 s, then stop.
* Set a client timeout well above the usual answer time. The examples use 120 seconds for `/extract`.
* After a timeout on `/extract` you do not know whether the read ran. Check **Documents** for the `reference` before you resend, or accept the duplicate.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.