# Error responses | Rijwind docs

# Error responses

Every non-2xx response from `api.rijwind.com` returns the same JSON
envelope:

```json
{
  "error": "machine_readable_code",
  "message": "Human-readable explanation."
}
```

You can rely on:

- `error` is a stable identifier — match against it in code.
- `message` is for humans — show it to the developer, but don't switch
  on it in code (we may rewrite it for clarity).
- The HTTP status code is set correctly. Treat any `4xx`/`5xx` as a
  failure even if you can't recognise the `error` value yet.

## Codes you'll see

### 400 Bad Request

The request shape is malformed or invalid. Fix the request and retry.

| `error`       | Cause                                                                                    |
| ------------- | ---------------------------------------------------------------------------------------- |
| `bad_request` | The request body or query is invalid. Branch on the `message` field for the sub-reason — common ones are listed below. |

The `error` code is always `bad_request` for 4xx-shape problems; the
human-readable `message` distinguishes between sub-cases. Examples:

- `"bad request: costing 'truck' is not supported; allowed: auto, bicycle, pedestrian"` — the `costing` field is not one of `auto`, `bicycle`, or `pedestrian`.
- `"bad request: matrix request too large: 60 × 60 = 3600 pairs exceeds the 2500 cap"` — `len(sources) × len(targets) > 2500`. Split into multiple smaller matrix calls.
- `"bad request: isochrone request too large: 2 × 3 = 6 pairs exceeds the 4 cap"` — `len(locations) × len(contours) > 4`. Run separate calls per origin or contour.
- `"bad request: invalid JSON"` — the body could not be parsed, or a required field is missing.

Match on `error === "bad_request"` in code; treat `message` as human-readable only.

400s **never charge quota**.

### 401 Unauthorized

The Bearer key is missing or unknown.

| `error`       | Cause                                                              |
| ------------- | ------------------------------------------------------------------ |
| `missing_key` | No `Authorization` header (and no `?key=` fallback) on the request. |
| `invalid_key` | The key doesn't exist in our index. Likely a typo.                 |

Revoked keys return `403 key_revoked` — see the 403 section below.

401s never charge quota.

### 403 Forbidden

The key is valid but the request is not authorised — either it came
from a blocked origin or the endpoint is outside the key's scope.

| `error`              | Cause                                                                                     |
| -------------------- | ----------------------------------------------------------------------------------------- |
| `origin_not_allowed` | The request's `Origin`/`Referer` didn't match the project's allowed-origins whitelist.    |
| `origin_required`    | The project requires an `Origin`/`Referer` and neither was sent.                          |
| `key_revoked`        | The key has been revoked from the dashboard.                                              |
| `scope_denied`       | The endpoint is outside the key's scope list. Reissue with the missing scope, or use a full-access key. |

403s never charge quota.

If a server-side script trips `origin_not_allowed`, either drop the
`Origin` header (most HTTP clients do that by default) or clear the
allowed-origins list on that key.

### 429 Too Many Requests

The monthly quota is exhausted (hard cap) **or** an upstream component
is shedding load.

| `error`             | Cause                                                            |
| ------------------- | ---------------------------------------------------------------- |
| `quota_exhausted`   | You hit `monthly_request_limit` and the plan is on `cap_mode=hard`. Resets at the end of your current 30-day cycle — anchored on your billing anniversary (paid plans) or email-verification date (free plan). Your dashboard shows the exact next-reset timestamp. |

### 502 Bad Gateway

A backend service timed out or is restarting. This is transient —
retry with exponential backoff. We log every 502 on our side and
don't need you to file a ticket unless they persist for more than
a few minutes.

### 503 Service Unavailable

Brief planned-maintenance windows for data refreshes — see status
updates on [rijwind.com](https://rijwind.com) for timing. Same retry
advice as 502.

## Retrying safely

- **Read endpoints** (`GET /tiles/v1/token`, `/search/geocode/v1/*`) are
  idempotent — retry freely.
- **Routing endpoints** (`POST /directions/v1`, `/directions-matrix/v1`,
  `/isochrone/v1`) are also idempotent in the absence of side effects;
  re-sending the same request always produces the same answer. Retry
  freely on 502/503.
- For 429: don't retry tighter than the start of the next quota
  period. The dashboard shows your reset timestamp.
