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

# API Error Codes

> Every Off the Hook API error returns a stable error code, a human-readable message, and a requestId for support. Switch on the code in your error handling.

Every error response from the Off the Hook API shares the same JSON shape. The `error` field contains a stable string code that you can safely switch on in your code — these codes are never repurposed across versions. The `message` field is a human-readable description intended for logging and debugging, not for display in your UI.

## Error response shape

```json theme={null}
{
  "error": "not_found",
  "message": "subscription not found",
  "detail": null,
  "requestId": "host/reqId-000001"
}
```

| Field       | Type           | Description                                                                  |
| ----------- | -------------- | ---------------------------------------------------------------------------- |
| `error`     | string         | Stable error code enum — safe to switch on in code                           |
| `message`   | string         | Human-readable description of the error                                      |
| `detail`    | object \| null | Additional structured context for select error codes; `null` for most errors |
| `requestId` | string         | Unique identifier for the request — include this when contacting support     |

<Tip>
  The `requestId` value is also sent as the `X-Request-Id` response header on every API response, including successful ones. You can extract it from the header without parsing the response body, which is useful in middleware or logging layers.
</Tip>

## Error codes

| Code                       | HTTP status | Description                                                       |
| -------------------------- | ----------- | ----------------------------------------------------------------- |
| `unauthorized`             | 401         | Missing, malformed, or invalid API key                            |
| `forbidden`                | 403         | Valid key but no access to this resource                          |
| `not_found`                | 404         | Subscription or event does not exist                              |
| `invalid_request`          | 400         | Request body or parameter is malformed                            |
| `invalid_address`          | 400         | Address is not a valid TRON base58check address                   |
| `invalid_chain`            | 400         | Chain ID is not a recognized CAIP-2 identifier                    |
| `chain_not_enabled`        | 400         | Chain ID is valid but not yet active                              |
| `ssrf_blocked`             | 400         | Destination URL resolves to a blocked private IP range            |
| `unsupported_event`        | 400         | Event kind is not recognized or not yet supported                 |
| `description_too_long`     | 400         | Description field exceeds the character limit                     |
| `addresses_limit_exceeded` | 400         | Adding these addresses would exceed the per-subscription limit    |
| `idempotency_in_flight`    | 409         | A request with the same `Idempotency-Key` is still processing     |
| `idempotency_conflict`     | 409         | A completed request with the same key returned a different result |
| `invalid_pagination_token` | 400         | The `nextPageToken` value is malformed or expired                 |
| `rate_limited`             | 429         | Too many requests — slow down and retry                           |
| `internal`                 | 500         | Unexpected server error; include `requestId` when reporting       |

## Error code details

### `ssrf_blocked`

The `ssrf_blocked` error includes a `detail` object that identifies exactly which IP and CIDR block caused the rejection. This error occurs both at subscription creation time (when the destination URL is validated) and at delivery time (as a defense against DNS rebinding).

```json theme={null}
{
  "error": "ssrf_blocked",
  "message": "url resolves to a blocked range 10.0.0.0/8",
  "detail": { "ip": "10.0.0.5", "cidr": "10.0.0.0/8" },
  "requestId": "..."
}
```

Blocked ranges include `10.0.0.0/8`, `172.16.0.0/12`, `192.168.0.0/16`, `127.0.0.0/8`, and other reserved private address spaces. Use a publicly routable HTTPS URL as your webhook destination. During development, [https://webhook.site](https://webhook.site) provides a free public endpoint.

### `idempotency_in_flight`

Returned when you send a second request with an `Idempotency-Key` that is still being processed by a concurrent request. Wait a moment and retry; the original request will complete shortly.

### `idempotency_conflict`

Returned when a completed request exists for the given `Idempotency-Key` but its stored response differs from what the current request would produce. This typically means the request body changed between calls using the same key. Use a new unique key for the new request.

### `internal`

An unexpected server error. These are logged and investigated automatically, but including the `requestId` when you report the issue allows the team to find the exact request in the logs.
