# Errors

Status codes, the shapes an error body takes, and the codes an integration should handle

Section: api. Audience: public. Updated: 2026-10-09.

---
The API reports errors with standard HTTP status codes and a JSON body. Decide what to do from the status code first, then from the machine-readable code in the body. Never parse the human-readable message: its wording can change.

## Status codes

| Status | Meaning | What to do |
|---|---|---|
| `400` | The request was understood but its content is not acceptable: a missing or invalid field, a field you may not change. | Fix the request. Retrying it unchanged will fail again. |
| `401` or `403` | The key is missing, malformed, unknown or revoked; or the key is valid but not allowed here; or your plan does not include the action. | Read the `code` (below) to tell these apart. |
| `404` | The object does not exist, or it belongs to another account. Also returned for a page number past the end of a list. | Check the ID. Horizon does not tell you whether another account's object exists. |
| `409` | The request conflicts with the object's current state. | Read the `error_code`. |
| `429` | Too many requests (`throttled`). | Wait for `Retry-After` seconds and retry. |
| `5xx` | Something failed on Horizon's side. | Retry with backoff. |

A rejected key can come back as `401` or `403` depending on the endpoint. Do not branch on the status alone for authentication problems; use the `code`.

## Body shapes

There is not one envelope for every error. Expect one of these.

**A general refusal** carries a message in `detail`, and often a `code`:

```json
{
  "detail": "Invalid or inactive API key.",
  "code": "api_key_invalid"
}
```

**A validation error** names each field that failed, with a list of messages, and uses `non_field_errors` for a problem that is not about one field. For example:

```json
{
  "quantity": ["A valid integer is required."]
}
```

**An order refusal** carries an `error_code` in capitals, a `detail` message, and extra keys that depend on the code. Validation-style messages for the fields involved sit under each field's name, as above:

```json
{
  "error_code": "READ_ONLY_FIELDS",
  "detail": "These fields are kept by Horizon and cannot be changed through the API: status.",
  "read_only_fields": ["status"],
  "status": ["…"]
}
```

A robust client reads `code`, then `error_code`, and falls back to the status code when neither is present.

## Codes worth handling

### Authentication

| Code | Meaning |
|---|---|
| `api_key_header_invalid` | The `Authorization` header starts with `Api-Key` but is not `Api-Key <key>`. |
| `api_key_invalid` | The key is unknown or has been revoked. Do not retry; check which key is deployed. |
| `api_key_owner_inactive` | The account the key belongs to is no longer active. |
| `api_key_forbidden` | The key is valid, but it was used outside the documented API, for example to buy a label or change an order. Keys work only on the endpoints in this section. See [Authentication and keys](/api/authentication/). |

### Plan and billing

| Status | Code | Meaning |
|---|---|---|
| `403` | `subscription_required` | The account has no active subscription that covers this action. |
| `403` | `dimension_unavailable` | Your plan does not include this at all. |
| `403` | `grace_restricted` | A payment on the account failed. Some actions are paused until the payment details are updated. |
| `403` | `entitlement_lookup_failed` | Horizon could not work out your plan just then. Nothing was done. This is on Horizon's side; try again later. |

When a higher plan would lift the limit, the body also carries `upgrade_to` with the plan's `plan_key` and `plan_name`. Your allowances and current usage are in the app; see [Usage and limits](/app/usage-and-limits/).

### Orders

| Status | Code | Meaning |
|---|---|---|
| `400` | `READ_ONLY_FIELDS` | A new order set fields Horizon keeps. `read_only_fields` lists them. Nothing was saved. |
| `400` | `ADDRESS_INVALID` | A delivery address field of a new order fails the destination country's format. `fields` gives a code per field. |

[API keys](/app/api-keys/) lists the fields Horizon keeps and the address codes.

### Rate limiting

A `429` body carries `"code": "throttled"` and `retry_after`, the number of seconds to wait, which is also sent in the `Retry-After` header. `retry_after` can be null when no wait is known; back off before retrying anyway.