# Errors

> The one error envelope, every error code, and whether to retry.

Source: https://api.tailend.dev/docs/errors

Every failure returns the same JSON envelope. Never an HTML page, never a bare
string:

```json
{ "error": { "code": "not_found", "message": "unknown metro 'boston'", "request_id": "req_7431b51e10ec2b77" } }
```

| Field | Meaning |
| --- | --- |
| `code` | One value from a closed list (below). Branch on this. |
| `message` | A human-readable explanation. It can change; do not parse it. |
| `details` | Optional machine-readable context, such as the parameter that failed or the scopes your key has. |
| `request_id` | The id of this request. Quote it when you contact support. |

## Error codes

Branch on `code`, not the status alone: `403` and `429` each cover codes with
different remedies.

| Status | `code` | Retry? | What to do |
| --- | --- | --- | --- |
| 400 | `bad_request` | No | Fix the request; `details` usually names the parameter |
| 401 | `unauthorized` | No | Send a valid, unrevoked key in `X-API-Key` |
| 403 | `forbidden` | No | Your key is not scoped for this metro or type; `details` lists what it can read |
| 403 | `include_not_permitted` | No | Drop the `include=`, add `include_mode=lenient`, or use an entitled key |
| 403 | `feature_not_in_plan` | No | The endpoint needs a feature your plan does not include; `details.feature` names it |
| 404 | `not_found` | Only if the input changes | Check the metro, id or slug |
| 405 | `method_not_allowed` | No | Use `GET`; the API is read-only |
| 429 | `rate_limited` | Yes, after `Retry-After` seconds | Wait, then retry |
| 429 | `quota_exceeded` | No | The month's allowance is spent; `Retry-After` points at the start of next month |
| 500 | `internal` | Once, with backoff | If it recurs, contact support with the `request_id` |
| 501 | `not_implemented` | No | This feature is not available here; do not build on it |
| 503 | `unavailable` | Yes, after `Retry-After` | Back off and retry |

New codes can be added over time, so treat an unknown `code` by its status.

## Handling errors in code

```javascript
const res = await fetch(url, { headers: { "X-API-Key": process.env.TAILEND_API_KEY } });
if (!res.ok) {
  const { error } = await res.json();
  if (error.code === "rate_limited" || error.code === "unavailable") {
    const wait = Number(res.headers.get("Retry-After") ?? 1);
    // wait `wait` seconds, then retry once
  } else {
    console.error(error.code, error.message, error.request_id);
  }
}
```

## `request_id`

Every response, success or failure, carries a request id: in the
`X-Request-Id` header, and in the body of an error. Log it next to your own
request context and quote it when you contact support; it is how we find your
request.

## Checklist

- Check `error.code` before deciding whether to retry.
- On a `429`, read `Retry-After` instead of guessing a backoff: the two codes
  mean very different waits.
- Do not retry `400`, `401`, `403`, `404` or `501` unchanged.
