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

# Errors

> All Endstate API errors share a single envelope shape. Learn how to handle them reliably by branching on stable error codes.

## Overview

Every error response — regardless of endpoint or HTTP status — uses the same JSON envelope. Parse it once and reuse that logic across your integration.

***

## Error envelope

```json theme={null}
{
  "error": {
    "code": "validation.failed",
    "message": "Human-readable description; may change.",
    "request_id": "req_8e1a7f50-90ab-4cde-f012-3456789abcde",
    "doc_url": "https://docs.endstate.io/errors/validation-failed"
  }
}
```

For validation errors, an optional `details` array is included alongside the fields above:

```json theme={null}
{
  "error": {
    "code": "validation.failed",
    "message": "Request body failed schema validation.",
    "request_id": "req_8e1a7f50-90ab-4cde-f012-3456789abcde",
    "doc_url": "https://docs.endstate.io/errors/validation-failed",
    "details": [{ "field": "name", "issue": "Required field is missing." }]
  }
}
```

<ResponseField name="error.code" type="string">
  A stable, namespaced error code in `<resource>.<reason>` format (for example, `chip.not_found`). Branch your error-handling logic on this field — it is guaranteed not to change meaning.
</ResponseField>

<ResponseField name="error.message" type="string">
  A human-readable description of the error. Useful for logging and debugging,
  but **do not parse or match against this string** in code — its wording may
  change without notice.
</ResponseField>

<ResponseField name="error.request_id" type="string">
  A unique identifier for the request, formatted as `req_<uuid>`. This matches the `X-Request-Id` response header. Log it and include it in any support request so Endstate can locate the request in server-side logs.
</ResponseField>

<ResponseField name="error.doc_url" type="string">
  A URL pointing to the documentation for this error code (the [error code
  reference](#error-code-reference) below).
</ResponseField>

<ResponseField name="error.details" type="array">
  Present only on `validation.failed` responses. Each item identifies a specific
  field and the validation issue that failed.
</ResponseField>

***

## Best practices

**Branch on `error.code`, not on `message` or HTTP status.**

* `error.code` is stable and namespaced. It is the correct signal for programmatic branching.
* `error.message` is human-readable prose that may change without notice. Never parse or `switch` on it.
* HTTP status codes are a useful coarse signal (see below), but multiple distinct error codes share the same HTTP status. For example, both `auth.unauthorized` and `session_token.invalid_or_expired` return `401`. Branching on the status alone is not precise enough to handle them differently.

**Ignore unknown codes and fields.**

The API may introduce new error codes and new top-level fields on the `error` object over time. Write your error handler to treat any unrecognized `code` as a generic failure rather than throwing on an unexpected value.

**Log `request_id` on every error.**

The `request_id` field (also available as the `X-Request-Id` response header) uniquely identifies the server-side execution of that request. Always capture it alongside error details. When you contact support, include this value — it is the fastest way to locate and diagnose the problem.

***

## HTTP status classes

The Endstate API uses standard HTTP status semantics. Branch on the **class** (the hundreds digit) rather than exact status codes.

| Class | Meaning      | Action                                                                                                                                   |
| ----- | ------------ | ---------------------------------------------------------------------------------------------------------------------------------------- |
| `2xx` | Success      | Parse the response body normally.                                                                                                        |
| `4xx` | Client error | The request was rejected. Fix the request based on `error.code` before retrying.                                                         |
| `5xx` | Server error | The API encountered an unexpected condition. These are safe to retry with backoff. Log `request_id` and contact support if they persist. |

<Note>
  Plain HTTP requests receive a `426 Upgrade Required` response. HTTPS is
  required for all API calls.
</Note>

***

## Error code reference

### Authentication

| Code                | HTTP | When                                                                              |
| ------------------- | ---- | --------------------------------------------------------------------------------- |
| `auth.unauthorized` | 401  | Credential is missing or malformed.                                               |
| `auth.forbidden`    | 403  | Credential is valid but does not have access to the requested resource or action. |

<Note>
  Cross-organization resource access returns `404` rather than `403` to avoid
  revealing that the resource exists.
</Note>

### Session tokens

| Code                               | HTTP | When                                                                        |
| ---------------------------------- | ---- | --------------------------------------------------------------------------- |
| `session_token.invalid_or_expired` | 401  | The session token is unknown, expired, or has been revoked.                 |
| `session_token.wrong_chip`         | 403  | The session token is bound to a different chip than the one in the request. |

### Chips

| Code                   | HTTP | When                                                                                                                |
| ---------------------- | ---- | ------------------------------------------------------------------------------------------------------------------- |
| `chip.not_found`       | 404  | The chip ID does not exist in your organization.                                                                    |
| `chip.invalid_e_value` | 422  | The tap credential (`e` value) was rejected as invalid.                                                             |
| `chip.already_scanned` | 410  | The tap credential has already been used. Each tap produces a single-use credential — replay attempts are rejected. |
| `chip.not_a_test_chip` | 400  | A tap simulation was attempted on a real chip. The simulation endpoint is only available for test chips.            |
| `chip.already_paired`  | 409  | The chip is already paired to a unit and cannot be paired again.                                                    |

### Chip replacements

| Code                           | HTTP | When                                                                                      |
| ------------------------------ | ---- | ----------------------------------------------------------------------------------------- |
| `chip_replacement.locked`      | 409  | The unit has been claimed — its chip is permanently locked and can no longer be replaced. |
| `chip_replacement.in_progress` | 409  | A replacement is already open for this unit. Poll it until it settles.                    |
| `chip_replacement.not_found`   | 404  | No replacement with that ID exists for the unit.                                          |

### Units

| Code                  | HTTP | When                                                                                       |
| --------------------- | ---- | ------------------------------------------------------------------------------------------ |
| `unit.not_found`      | 404  | The unit ID does not exist in your organization.                                           |
| `unit.already_exists` | 409  | A unit with the supplied `external_id` already exists in your organization.                |
| `unit.not_minted`     | 409  | The unit has not been issued yet. Pair a chip and wait for `token.status: "active"` first. |

### Claims

| Code                         | HTTP | When                                                                                  |
| ---------------------------- | ---- | ------------------------------------------------------------------------------------- |
| `claim.in_progress`          | 409  | A claim is already open for this unit. Wait for it to settle before creating another. |
| `claim.already_to_recipient` | 409  | The unit is already owned by, or being claimed to, that address.                      |
| `claim.owner_unknown`        | 409  | The unit's current owner cannot be determined yet. Retry shortly.                     |
| `claim.not_found`            | 404  | No claim with that ID exists for the unit.                                            |

### Transfers

| Code                            | HTTP | When                                                                                     |
| ------------------------------- | ---- | ---------------------------------------------------------------------------------------- |
| `transfer.in_progress`          | 409  | A transfer is already open for this unit. Wait for it to settle before starting another. |
| `transfer.already_to_recipient` | 409  | The unit is already held by that address.                                                |
| `transfer.owner_unknown`        | 409  | The unit's current owner cannot be determined yet. Retry shortly.                        |
| `transfer.not_found`            | 404  | No transfer with that ID exists for the unit.                                            |

### Collections

| Code                        | HTTP | When                                                                                                             |
| --------------------------- | ---- | ---------------------------------------------------------------------------------------------------------------- |
| `collection.not_found`      | 404  | The collection ID does not exist in your organization.                                                           |
| `collection.already_exists` | 409  | A collection with the supplied `external_id` already exists in your organization.                                |
| `collection.not_active`     | 409  | The collection has not finished provisioning. Wait until its status is `active` before creating units inside it. |

### Generic and rate limiting

| Code                  | HTTP | When                                                                                                                 |
| --------------------- | ---- | -------------------------------------------------------------------------------------------------------------------- |
| `validation.failed`   | 400  | The request body failed schema validation. See `error.details` for per-field issues.                                 |
| `not_found.resource`  | 404  | Generic resource not found. The requested resource does not exist within your organization.                          |
| `quota.exceeded`      | 429  | Your organization has exceeded a usage quota. Contact support.                                                       |
| `rate_limit.exceeded` | 429  | Per-key rate limit exceeded.                                                                                         |
| `internal.error`      | 500  | An unexpected server error occurred. Retry with exponential backoff and include `request_id` in any support request. |

<Note>
  Specific rate limits and quotas are set per organization during onboarding.
  Treat any `429` as retryable with exponential backoff.
</Note>

***

## Next steps

<CardGroup cols={2}>
  <Card title="Pagination" icon="list" href="/conventions/pagination">
    Learn how to iterate through large result sets with cursor-based pagination.
  </Card>

  <Card title="API reference" icon="book-open" href="/api-reference/introduction">
    Full request and response schemas for every endpoint.
  </Card>
</CardGroup>
