Error reference
All errors carry a stable reason string in the response body, plus an HTTP status that approximates the same semantics. Branch on reason for programmatic handling; use the status for generic HTTP middleware.
Wire shape
HTTP
{
"error": "<human-readable message>",
"reason": "<stable_machine_string>"
}
X-RateLimit-Reason header echoes the same reason for 4xx responses. The standard Retry-After header (integer seconds, RFC 9110) is set on 429 rate and 429 concurrent responses. A sibling retry-after-ms header carries the same hint in milliseconds — useful when the bucket refill is sub-second (any rps_cap > 1); the standard header would round to 0 / 1 and mislead. balance and token_budget_exceeded 429s ship neither: they don't clear on a predictable schedule.
WebSocket
JSON-RPC error frame:
{
"jsonrpc": "2.0",
"id": <request id>,
"error": {
"code": 429,
"message": "<human-readable>",
"data": {
"reason": "<stable_machine_string>",
"http_status": 429,
"retry_after_ms": <number>
}
}
}
For rate-limit and balance classes, error.code is 429. Other error classes use JSON-RPC server-defined codes in -32000..-32099.
Reason catalog
401 Unauthorized
| reason | meaning |
|---|---|
invalid_token |
The token in the URL doesn't exist or has been revoked. |
signature_invalid |
The signed-message signature didn't recover to the claimed account pubkey. |
missing_headers |
A signed endpoint was called without X-Solidpeer-{Account,Timestamp,Signature}. |
timestamp_skew |
The X-Solidpeer-Timestamp is more than 300 s from server clock. |
replay_detected |
Same signature was already used inside the timestamp window. |
token_expired |
The token has a hard expiry and that time has passed. |
403 Forbidden
| reason | meaning |
|---|---|
method_not_in_allowlist |
The token's method allowlist excludes this RPC method. |
system_not_allowed |
The token isn't scoped to this system (bchn/fulcrum/chaingraph). |
network_not_allowed |
The token isn't scoped to this network. |
origin_denied |
The request's Origin header isn't on the token's origin allowlist. |
account_suspended |
The account is operator-flagged. |
account_expired |
Cycle ended without renewal. Renew to restore service. |
requires_paid_tier |
The action (e.g. minting a token) requires a paid tier. |
token_count_limit |
Account is at its tier's max active-token cap. |
429 Too Many Requests
| reason | meaning |
|---|---|
rate |
The per-account or per-token bucket is empty. Retry-After carries the suggested wait in seconds. |
concurrent |
The per-account in-flight HTTP request cap is full. Try again as in-flight calls return. |
balance |
Account CC balance is too low for the call's reservation. Top up or upgrade. |
token_budget_exceeded |
The token's per-token CC budget cap has been hit. Use a different token. |
400 / 500 / 502 / 503
| reason | meaning |
|---|---|
parse_error |
Request body wasn't valid JSON-RPC / GraphQL. |
preflight |
The request's parameters failed shape validation before forwarding. |
upstream_error |
The upstream node returned an error. Body contains the upstream message verbatim. |
no_upstream |
No healthy backend currently registered for the requested (system, network). Status endpoint shows live availability. |
subscriptions_unsupported |
Tried to open a subscription on a system that doesn't support them (bchn). |
Retry strategy
429 rate— honorRetry-After. Exponential back-off isn't necessary; the bucket refills at a predictable rate.429 balance/429 token_budget_exceeded— don't retry. Top up or rotate.503 no_upstream— short retry (≥1 s) is reasonable; upstream may recover.502 upstream_error— usually a transient upstream issue. One retry is fine; more is unlikely to help.401and403— don't retry. Fix the credential or scope.