# Errors

# Errors

The AirTrack API uses conventional HTTP status codes and a single, predictable error
envelope, so you can handle failures the same way everywhere.

## The error envelope

Every handled error returns a JSON body with a single `detail` string describing what
went wrong:

```json
{ "detail": "Your token lacks the required scope" }
```

Every handled status uses this shape, including `422` — where `detail` is a fixed
`"Request validation failed"` for body validation, because the offending input is
deliberately not echoed back. The one exception is **`500`**, which returns the plain text
`Internal Server Error`.

## Status codes

| Status | Meaning | Body |
|--------|---------|------|
| `400` | Bad request — for example an invalid forecast date, a route that exceeds the maximum plan distance, or waypoint times outside the window air-quality data exists for. | `{ "detail": … }` |
| `401` | Missing, unknown or revoked token; or an invalid/expired pairing code. | `{ "detail": … }` |
| `403` | Your token isn't permitted to perform this operation — usually a missing scope, or the owning user no longer holding a premium plan. The exact causes vary per endpoint; see its own description. | `{ "detail": … }` |
| `404` | No such resource, or it belongs to a different AirTrack user — for example polling a route-plan `request_id` created by someone else. Plans are owned by the user, not the connection, so your other connections can poll them. | `{ "detail": … }` |
| `422` | The request failed validation — the body, or a path parameter such as an invalid `request_id`. | `{ "detail": … }` |
| `429` | Rate limited, or your credit pool (monthly or daily burst) is exhausted. | `{ "detail": … }` + `Retry-After` |
| `503` | An upstream data source was unavailable — the call was not charged. | `{ "detail": … }` |

## Rate limits & credits

Endpoints that compute air quality draw from one shared monthly **credit pool**, with a
smaller **daily burst cap** so the whole month can't be spent at once — costs per
endpoint are on the [Usage & credits](/credits) page. Redeeming a
pairing code, reading `GET /v1/quota` and polling for a route-plan result are free.
When either limit is reached, calls return `429` with a `Retry-After` header giving the
number of seconds to wait.
Check your current balance any time with [`GET /v1/quota`](/api/quota#get-quota).

Calls that fail **before delivering a result** — a `503` upstream outage, or a route
plan rejected for being too long — are **refunded automatically**, so a failed request
never costs you credits.

## Handling errors in code

Branch on the status code, honour `Retry-After` on a `429`, and read `detail` for a
human-readable reason. This helper retries rate-limited calls and raises on anything
else:

<CodeTabs>

```python title="Python"
import time
import requests

def call_with_retry(session, method, url, *, attempts=3, **kwargs):
    for _ in range(attempts):
        r = session.request(method, url, **kwargs)
        if r.status_code == 429:
            time.sleep(int(r.headers.get("Retry-After", "1")))
            continue
        if not r.ok:
            detail = r.json().get("detail", r.text) if "json" in r.headers.get(
                "content-type", ""
            ) else r.text
            raise RuntimeError(f"{r.status_code}: {detail}")
        # 204 has no body — don't try to parse one.
        return r.json() if r.status_code != 204 else None
    raise RuntimeError("Rate limited — retries exhausted")
```

```typescript title="TypeScript"
async function callWithRetry(path: string, init: RequestInit, attempts = 3) {
  for (let i = 0; i < attempts; i++) {
    const res = await fetch(`${BASE}${path}`, init);
    if (res.status === 429) {
      const wait = Number(res.headers.get("Retry-After") ?? "1");
      await new Promise((r) => setTimeout(r, wait * 1000));
      continue;
    }
    if (!res.ok) {
      const body = await res.json().catch(() => ({}));
      throw new Error(`${res.status}: ${body.detail ?? res.statusText}`);
    }
    // 204 has no body — don't try to parse one.
    return res.status === 204 ? null : res.json();
  }
  throw new Error("Rate limited — retries exhausted");
}
```

</CodeTabs>

For how to obtain and secure a token, see [Authentication](/authentication). For
runnable examples of each endpoint, see the [Quickstart](/quickstart).
