# Quotas and errors

> Monthly quotas per plan, the X-Quota headers, rate limits, and every error status and body of api.geolith.app.

Each plan has a monthly request quota and a per-second rate limit per key. Over quota, requests stop with `429 quota_exceeded` until the month ends or you upgrade. There are no overage charges.

## Quotas

| Plan | Price | Requests a month |
| --- | --- | --- |
| Free | $0 | 100,000 |
| Starter | $19/month | 1,000,000 |
| Growth | $99/month | 7,500,000 |
| Scale | $299/month | 25,000,000 |

Every metered endpoint counts as one request: search, autocomplete, structured, reverse, nearby, place, TileJSON and tiles. `GET /health` and `OPTIONS` preflight requests are free. The month is the calendar month in UTC. Card checkout is not live yet; for a paid plan, email contact@geolith.app.

Every answered request carries two headers:

| Header | Meaning |
| --- | --- |
| `X-Quota-Limit` | Requests a month on your plan |
| `X-Quota-Remaining` | Requests left this month |

## Errors

Key, origin, rate and quota errors are JSON with a stable `code`:

```json
{ "error": { "code": "missing_api_key", "message": "Pass an API key as ?api_key= or an Authorization: Bearer header." } }
```

| Status | Code | When | What to do |
| --- | --- | --- | --- |
| 400 | (`geocoding.errors`) | A parameter is missing or invalid | Fix the request |
| 401 | `missing_api_key` | No key | Send `api_key` or `Authorization: Bearer` |
| 401 | `invalid_api_key` | Malformed, unknown or revoked key | Check the key in the dashboard |
| 403 | `origin_not_allowed` | Publishable key from an origin it does not list | Add the origin, or use a secret key on a server |
| 404 | `not_found` | Unknown path or tile set | Check the path |
| 405 | `method_not_allowed` | Not `GET`, `HEAD` or `OPTIONS` | Use `GET` |
| 429 | `rate_limited` | Too many requests per second on one key | Wait for `Retry-After` (1 s) and retry |
| 429 | `quota_exceeded` | The monthly quota is used up | Wait for the next month or upgrade |
| 502 | (`geocoding.errors`) | The index could not be read | Retry later |

Parameter errors keep the GeoJSON shape, so a client that reads `features` gets an empty list:

```json
{ "geocoding": { "errors": ["missing param 'text'"] }, "type": "FeatureCollection", "features": [] }
```

## Retrying

Retry `429 rate_limited` and `502` with a short back-off. Do not retry `400`, `401`, `403` or `429 quota_exceeded`.

```js
async function geolith(url, init, tries = 4) {
  for (let i = 0; i < tries; i++) {
    const res = await fetch(url, init);
    if (res.status === 429) {
      const body = await res.clone().json();
      if (body.error?.code === 'quota_exceeded') return res;
      await new Promise((r) => setTimeout(r, Number(res.headers.get('Retry-After') ?? 1) * 1000));
      continue;
    }
    if (res.status >= 500) {
      await new Promise((r) => setTimeout(r, 500 * 2 ** i));
      continue;
    }
    return res;
  }
  throw new Error('geolith: gave up after retries');
}
```
