Errors
Error envelope shape, error codes, retry guidance, and rate-limit headers.
Every API error uses one error envelope with a machine code, a human message, and the request id.
{
"error": {
"code": "NOT_FOUND",
"message": "Vault not found",
"request_id": "3f6c1a2e-9b7d-4c1e-8f2a-1d2e3f4a5b6c"
}
}Codes are uppercase. During the one-release compatibility window for the PHI-295 migration, a response whose code changed from a legacy lowercase value also carries that previous value as error.legacy_code.
{
"error": {
"code": "UNAUTHORIZED",
"legacy_code": "invalid_api_key",
"message": "API key is invalid or has been revoked",
"request_id": "3f6c1a2e-9b7d-4c1e-8f2a-1d2e3f4a5b6c"
}
}Request Ids
Every response carries an X-Request-Id header, and every error body carries the same value as request_id. Quote it when reporting a failure: support can find the request, its key, endpoint, status, and latency from that id alone.
Send your own X-Request-Id (1 to 64 letters, digits, underscores, or dashes) and it is echoed back unchanged, so you can correlate with your own logs; any other value is replaced with a generated id. Error responses are sent with Cache-Control: no-store, so a request id is never replayed from a cache.
Error Codes
| Code | HTTP status | Meaning | Retry guidance |
|---|---|---|---|
BAD_REQUEST | 400 | Invalid path, query, or body input | Fix the request before retrying |
UNAUTHORIZED | 401 | Authentication is missing or invalid | Add a valid bearer key or session |
FORBIDDEN | 403 | The supplied key or session is not allowed | Do not retry until access changes |
NOT_FOUND | 404 | Resource not found or feature disabled | Check the identifier or feature availability |
CONFLICT | 409 | Request conflicts with existing state | Refresh state before retrying |
GONE | 410 | A resource is no longer available | Follow a successor link when one is supplied |
VERSION_RETIRED | 410 | Basket version was retired | Follow the successor URL |
PAYLOAD_TOO_LARGE | 413 | Request body exceeds the API body limit | Send a smaller body |
UNSUPPORTED_MEDIA_TYPE | 415 | Request content type is unsupported | Send a supported Content-Type |
RATE_LIMITED | 429 | Rate limit exceeded | Retry after the Retry-After value |
SUBCLIENT_RATE_LIMITED | 429 | A shared-key integration's per-client budget was exceeded | Retry after the Retry-After value |
ONDO_API_UNAVAILABLE | 502 | The upstream Ondo API could not serve an admin preview | Retry with backoff |
SERVICE_UNAVAILABLE | 503 | Fail-safe, graph materialization, Redis, or upstream data is unavailable | Retry after the Retry-After value when present |
INTERNAL_ERROR | 500 | Unexpected server error | Retry with backoff and include request_id if reporting |
GATEWAY_TIMEOUT | 504 | A read exceeded its time budget | Retry after the Retry-After value |
The route-specific codes in the table stay uppercase and use the same envelope.
For one compatibility release, these former lowercase values appear only as error.legacy_code:
| Legacy code | Canonical code |
|---|---|
invalid_api_key | UNAUTHORIZED |
tier_required | UNAUTHORIZED without a key; otherwise FORBIDDEN |
too_many_connections | RATE_LIMITED |
bad_request | BAD_REQUEST |
validation_error | BAD_REQUEST |
not_found | NOT_FOUND |
royco_market_not_indexed | NOT_FOUND |
internal_error | INTERNAL_ERROR |
forbidden | FORBIDDEN |
feature_not_enabled | FORBIDDEN |
upgrade_required | FORBIDDEN |
key_limit_reached | CONFLICT |
overloaded | SERVICE_UNAVAILABLE |
unavailable | SERVICE_UNAVAILABLE |
signing_unavailable | SERVICE_UNAVAILABLE |
VERSION_RETIRED is emitted by immutable basket version routes. This retired-version response is a legacy exception that includes the field success set to false alongside the error object and includes error.request_id. Like other errors, it is sent with Cache-Control: no-store. Its X-Request-Id header still identifies the current request. Parse error.code and use the successor pointer plus the Link header with rel="successor-version".
{
"success": false,
"error": {
"code": "VERSION_RETIRED",
"message": "Basket version 42 for \"rwa-tbill-conservative\" was retired. Use the latest version at /v1/baskets/rwa-tbill-conservative.",
"retired_at": "2026-07-01T00:00:00Z",
"successor_url": "https://api.philidor.io/v1/baskets/rwa-tbill-conservative",
"request_id": "3f6c1a2e-9b7d-4c1e-8f2a-1d2e3f4a5b6c"
}
}Rate Limit Headers
Responses include rate-limit headers when rate limiting is applied.
| Header | Meaning |
|---|---|
X-RateLimit-Limit | Requests allowed in the active window |
X-RateLimit-Remaining | Requests remaining in the active window |
X-RateLimit-Reset | Unix timestamp for reset |
X-RateLimit-Window | Window length such as 60s |
Retry-After | Seconds to wait before retrying |
X-Request-Id | Correlation id, also in every error body |
Anonymous public reads use IP-based limits. API-key requests use the key's configured per-minute limit on public routes.
Retry Pattern
Handle 429 and 503 first. If Retry-After is present, wait that many seconds before retrying.
Use exponential backoff for 500 responses. Do not retry 400, 401, 403, or 404 without changing the request or credentials.