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
{
"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 |
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.
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).
{
"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 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.