---
title: Errors
description: The error envelope every Gates v1 endpoint returns, and the codes it can carry.
---

Errors returned by the Gates v1 API use the same JSON body: a stable, machine-readable `error` code and a human-readable `message`.

```json
{
  "error": "gate_not_found",
  "message": "Gate not found."
}
```

Match on `error`, not the wording of `message`, which can change.

## Handling failures

Read the HTTP status first. Parse JSON defensively: an empty, invalid, or non-object body means no error code is available. For Gates v1, use this policy:

| Response | What to do |
|---|---|
| `429` with `quota_exceeded` | Check your credits or enable overage. Repeated retries will not restore the quota |
| Any other `429` | Pause requests. Honor `Retry-After` when present; otherwise use exponential backoff with jitter |
| `500`, `502`, `503`, `504` | Retry with backoff, even if the body is empty or has no error code |
| Other `4xx` | Fix the request or access problem before retrying |

Limit retry attempts. Only automatically retry requests that are safe to repeat: a failed response does not prove that a write was never applied. A missing `Retry-After` header alone does not mean credits are exhausted.

## Errors before the API

Infrastructure failures can return `502`, `503`, or `504` with JSON, text, HTML, or no body. The handling policy above works without a JSON error code. Temporary IP blocks return `429 rate_limit_exceeded` in the Gates v1 format and follow the same retry policy; see [Rate Limits](/docs/api/rate-limits#temporary-ip-blocks).

Cloudflare security rejections can return a non-JSON `403`, such as `error code: 1010`. This does not indicate an invalid API key; `403 pro_only` specifically means the account needs a plan that includes Gates. For an unexplained security rejection, [contact us](/contact) with the request URL (without credentials), timestamp, user-agent, and `CF-Ray` header if present. Do not include your API key.

## Field validation

When individual fields are at fault, the body carries an `errors` object naming each one:

```json
{
  "error": "validation_failed",
  "message": "Some input fields are invalid.",
  "errors": {
    "name": ["The name field is required."]
  }
}
```

## Codes on every endpoint

| Status | Code | Meaning |
|---|---|---|
| `401` | `unauthorized` | The API key is missing or invalid |
| `403` | `pro_only` | The account is not on a plan that includes Gates |
| `404` | `not_found` | No Gates endpoint at that path |
| `405` | `method_not_allowed` | The path exists, but not for that HTTP method. The response carries an `Allow` header listing the methods it does accept |
| `422` | `validation_failed` | One or more fields are invalid, see [Field validation](#field-validation) |
| `429` | `rate_limit_exceeded` | Throughput limit exceeded or a temporary IP block. See [Rate Limits](/docs/api/rate-limits) |
| `429` | `quota_exceeded` | Monthly [credits](/docs/get-started/credits) are exhausted until the billing cycle resets or overage is enabled |
| `500` | `server_error` | The request failed on our side |
| `502`, `503`, `504` | Not guaranteed | Infrastructure failure; the body may be empty or outside the JSON envelope. See [Errors before the API](#errors-before-the-api) |

## Codes on specific endpoints

The [Decision Endpoint](/docs/gates/decision-endpoint#error-responses) and the [Management API](/docs/gates/management-api#errors) each list the codes only they return, such as `invalid_email` or `rule_limit_reached`.

## The /v0 endpoint

`POST /v0/gates/{gate_id}/decisions` predates this contract and keeps its own:

- Input errors return `400` where `/v1` returns `422`. The `error` codes are the same on both.
- Exhausted monthly credits return `429 rate_limit_exceeded` where `/v1` returns `429 quota_exceeded`.
- Some error bodies include a `meta` object; do not require it on every failure.
- A wrong path or HTTP method returns the framework's default body rather than the envelope.

IP validation also applies to `/v0`: malformed `ip` values that previously returned `200` now return `400 invalid_ip` (`422 invalid_ip` on `/v1`). This adds an error code while preserving the `/v0` error response shape and input-error status. On both versions, `ip: null` is treated as absent and adds no IP signals or lookup credits.

[Migrate from v0 to v1](/docs/gates/migrate-from-v0) covers the move.
