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

# Errors

> Status codes, the error body, what is safe to retry, and why a rejection is not an error.

When a request fails, the API returns an HTTP error status and a body with one field:

```json lines theme={null}
{
  "error": "config.transaction_ttl_minutes: must be at most 1440 (24h)"
}
```

`error` is a human-readable message written for your engineers. It names the offending
field and the constraint it broke. It is not localised and it is not meant for your end
users.

## Status codes

| Status | Meaning | What to do |
| - | - | - |
| `400` | The request was understood but rejected — a missing field, a value out of range, a rule violated | Fix the request. Do not retry as-is. |
| `401` | The access token is missing, invalid or expired | Request a new token and retry once |
| `403` | The token is valid but not allowed to do this | Check the credentials and scope you are using |
| `404` | The transaction does not exist in your account | Check the id |
| `429` | Too many requests | Back off and retry with jitter |
| `5xx` | Something failed on our side | Retry with exponential backoff |

## Validation errors

Most `400`s come from the transaction configuration. The message tells you exactly which
constraint failed:

| Example message | Cause |
| - | - |
| `customer_id: is required` | A required field was omitted |
| `config.transaction_ttl_minutes: must be at most 1440 (24h)` | TTL outside 1–1440 |
| `config.webhook.url: must be a valid HTTPS URL` | Webhook URL is `http://` or malformed |
| `config.biometric_policy.liveness_min_score: must be <= 1.0` | Score outside 0.1–1.0 |
| `config.biometric_policy.expected_max_age: must be <= 200` | Age outside 0–200 |
| `config.document_policy.document_eligibility.target_documents[0].country: is required` | A target document is missing a field |

Every range and default is listed on [Create transaction](/api-reference/transactions/create-transaction).

<Info>
  A `400` on **Create transaction** means nothing was created. There is no transaction to
  clean up and no `sdk_token` was issued.
</Info>

## What is safe to retry

<Steps>
  <Step title="4xx: never retry unchanged">
    The request itself is wrong. Retrying the same bytes returns the same error. The one
    exception is `401`, where requesting a new token and retrying **once** is correct.
  </Step>

  <Step title="429 and 5xx: retry with backoff">
    Start around one second, double each time, add jitter, and give up after a handful of
    attempts. A `5xx` that persists for more than a minute is worth an alert on your side.
  </Step>

  <Step title="Timeouts: be careful with Create transaction">
    Create transaction is **not idempotent**. If your request timed out you do not know
    whether a transaction was created. Retrying blindly can leave an orphaned transaction
    that expires on its own — harmless, but it will show up in reconciliation. Prefer a
    generous client timeout over aggressive retries here.
  </Step>
</Steps>

```typescript lines theme={null}
async function withRetry<T>(fn: () => Promise<Response>, attempts = 4): Promise<Response> {
  let delay = 1_000;
  for (let i = 0; ; i++) {
    const res = await fn();
    const retriable = res.status === 429 || res.status >= 500;
    if (!retriable || i === attempts - 1) return res;
    await sleep(delay + Math.random() * 250);
    delay *= 2;
  }
}
```

## A rejection is not an error

This trips up almost every first integration, so it is worth stating plainly:

| Situation | HTTP status | Where you see it |
| - | - | - |
| The request was malformed | `400` | `error` field in the response body |
| The user **failed** verification | `200` | `result_data.decision: "REJECTED"` |
| The analysis **could not run** | `200` | `result_data.decision: "UNDETERMINED"` |

A `REJECTED` decision is a successful API call that returned a negative verdict. An
`UNDETERMINED` decision is a successful API call telling you the analysis did not
complete — a configuration problem or a provider failure, described in `issues[]`.
Neither is an HTTP error, and your error handling should not treat them as one.

See [Decisions and issues](/concepts/decisions-and-issues) for how to act on each.

## Getting help

When you contact support about a failed request, include the `transaction_id` if you have
one, the exact `error` message, and the time of the request in UTC. That is enough for us
to find it.


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