# Errors

> Error envelope shape, error codes, retry guidance, and rate-limit headers.

Most API errors use an `error` envelope with a machine code and a human message.

```json
{
  "error": {
    "code": "NOT_FOUND",
    "message": "Vault not found"
  }
}
```

## 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                               |
| `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                                         |
| `RATE_LIMITED`        | 429         | Rate limit exceeded                                                      | Retry after the `Retry-After` value                         |
| `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 context if reporting |

`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. Parse `error.code` and use the successor pointer plus the `Link` header with `rel="successor-version"`.

```json
{
  "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"
  }
}
```

## 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         |

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.