> **Can't find what you're looking for?** Use `search_docs` on the docs MCP server at `https://www.openfort.io/api/mcp` to find what you need.
>
> **Have feedback?** Use `submit_feedback` on the same MCP server.

# Errors

Openfort uses conventional HTTP status codes to signal the result of a request:

* **`2xx`** — the request succeeded.
* **`4xx`** — the request failed with the information given (a missing parameter, a bad credential, an unknown resource).
* **`5xx`** — something went wrong on Openfort's side.

## Error response format

Every error returns the same JSON envelope:

```json
{
  "error": {
    "type": "invalid_request_error",
    "message": "Validation Failed",
    "details": {
      "requestBody.chainId": {
        "message": "invalid integer number",
        "value": "abc"
      }
    }
  }
}
```

| Field | Type | Description |
| --- | --- | --- |
| `error.type` | string | A stable, machine-readable category. Branch on this. |
| `error.message` | string | A human-readable explanation. May change without notice — don't parse it. |
| `error.details` | object | Present on validation errors (`422`). Keyed by the path of the offending field, each with a `message` and, when the field was supplied, the rejected `value`. |
| `error.code` | string | Present on some errors that disambiguate a specific cause. |
| `error.param` | string | Present alongside `error.code`, naming the request field that caused it. |

`error.type` is one of exactly two values:

| Type | Sent with | Meaning |
| --- | --- | --- |
| `invalid_request_error` | every `4xx` | The request was rejected with the information given. Fixing the request is on you. |
| `api_error` | every `5xx` | Something failed on Openfort's side. The request may be fine. |

:::tip
Branch your code on the **HTTP status code** and `error.type`, never on `error.message` — messages are human-readable text and can change at any time.
:::

## Status codes

| Status | Meaning | What to do |
| --- | --- | --- |
| `400` | Bad Request — the request was malformed or a parameter was invalid. | Fix the request per `error.message` and retry. |
| `401` | Unauthorized — the credential is missing, malformed, or for the wrong mode. See [Authentication](https://www.openfort.io/docs/api-reference/authentication). | Send a valid key or token. |
| `403` | Forbidden — the credential is valid but not permitted to perform this request, usually a secret key missing a [scope](https://www.openfort.io/docs/api-reference/authentication). | Use a credential with the required access. |
| `404` | Not Found — the route or resource does not exist (or isn't visible to this key's mode). | Check the id and that you're using the matching test/live key. |
| `409` | Conflict — the request conflicts with the current state (for example, a duplicate). | Reconcile state, then retry. |
| `422` | Unprocessable Entity — schema validation failed. `error.details` names every offending field. | Fix the fields listed in `error.details`. |
| `429` | Too Many Requests — you exceeded the rate limit. See [Rate limits](#rate-limits). | Wait for `Retry-After` seconds, then retry. |
| `500` | Internal Server Error — an unexpected error on Openfort's side. Transient. | Retry with exponential backoff. |

## Rate limits

The API is rate limited **per project environment, per minute**. Test and live count separately, so a test-mode burst never eats into your live budget. The limit is set by your plan:

| Plan | Requests per minute |
| --- | --- |
| Free | 100 |
| Growth | 300 |
| Pro | 600 |
| Scale | 1,200 |
| Enterprise | Unlimited |

Exceeding the limit returns `429` with a **`Retry-After`** header giving the number of seconds to wait:

```bash
HTTP/1.1 429 Too Many Requests
Retry-After: 60
```

```json
{
  "error": {
    "type": "invalid_request_error",
    "message": "Rate limit exceeded for your current plan. Please upgrade for higher limits. Retry after 60 seconds."
  }
}
```

:::warning
`Retry-After` is the only rate-limit header the API sends, and it appears only on a `429`. There is no header telling you how much of your budget is left, so track your own request rate rather than waiting to be throttled.
:::

The bundler, paymaster, and Solana RPC endpoints are metered separately from the REST API, each with their own per-minute limit.

## Retrying safely

`5xx` responses are transient — retry with exponential backoff. Read requests (`GET`) are always safe to retry. For a write (`POST`, `PUT`, `DELETE`) that returns `5xx`, the operation may still have succeeded on the backend: treat the outcome as unknown and re-read the resource before retrying.

On a `429`, wait the full `Retry-After` before retrying — retrying sooner just consumes another rejected request.

## Request id

Every response carries an **`x-request-id`** header identifying that single request in Openfort's logs and traces:

```bash
x-request-id: 4bf92f3577b34da6a3ce929d0e0e4736
```

Log it alongside your own errors, and include it when you contact support — it is the fastest way to find the exact call you're asking about. Browser clients can read it: the header is exposed via CORS.

You can also **send** `x-request-id` yourself to correlate a call with your own tracing. Openfort adopts your value when it is a valid trace id, and always echoes back the id it resolved — so read the response header rather than assuming your value was kept.
