# Errors & limits

## Error format

Errors raised by the API itself have one JSON shape:

```json
{"error": "missing or invalid API key"}
```

The one exception is parameter validation (`422` for a missing, malformed or out-of-range
parameter), which uses the framework format:

```json
{"detail": [{"type": "less_than_equal", "loc": ["query", "limit"],
             "msg": "Input should be less than or equal to 100",
             "input": "101", "ctx": {"le": 100}}]}
```

## Status codes

| Status | Body | When | What to do |
| --- | --- | --- | --- |
| `200` | the result | Success. | — |
| `401` | `{"error": "missing or invalid API key"}` | No `X-API-Key` header, or the key is unknown, revoked or replaced. | Check the header and the key. |
| `402` | `{"error": "API key inactive: trial ended or payment due"}` | The key is known but not active: its trial ended or a payment is due. | [Contact us](https://jobs-database.de/contact) to (re)activate it. |
| `404` | `{"error": "job not found"}` | `GET /v1/jobs/{job_id}` with an id that does not exist. | Nothing — it costs nothing. |
| `422` | `{"error": "invalid cursor"}` | A `cursor` this API did not issue. | Restart without `cursor`. |
| `422` | `{"error": "q must be at least 3 characters"}` | Search term shorter than 3 characters after trimming. | Use a longer `q`. |
| `422` | `{"detail": [ … ]}` | A parameter is missing, has the wrong type or is out of range (e.g. `limit=101`, missing `q`, `q` over 200 characters, non-integer `job_id`). | Fix the parameter. |
| `429` | `{"error": "rate limit exceeded"}` + `Retry-After` header | Too many requests in the current minute. | Wait `Retry-After` seconds. |
| `429` | `{"error": "monthly allowance used up"}` (no `Retry-After`) | No jobs left in this month's allowance. | Wait for the 1st of next month (00:00 UTC) or [upgrade your plan](https://jobs-database.de/pricing). |
| `500` | `{"error": "internal"}` | Unexpected server error. | Retry later; contact us if it repeats. |
| `503` | `{"error": "service temporarily unavailable"}` | A backing service is unavailable or a query took too long. | Retry with exponential backoff. |

Only a `200` counts against your allowance. `401`, `402`, `404`, `422`, `429`, `500` and `503`
count nothing.

### Order of checks

A request is checked in this order; the first failing check decides the answer:

1. Parameters (`422`)
2. API key (`401`, `402`)
3. Per-minute rate limit (`429` with `Retry-After`)
4. Monthly allowance (`429` without `Retry-After`)
5. The query itself (`404`, `500`, `503`, or `200`)

## Rate limit

- **60 requests per minute per key.**
- The window is the **calendar minute in UTC** (it resets at second `:00`), not a rolling
  60 seconds.
- Every request that gets past the key check counts toward the minute — including requests that
  are themselves answered `429`.
- Over the limit you get `429 {"error": "rate limit exceeded"}` with a `Retry-After` header: the
  number of seconds (1–60) until the next minute starts.

## Monthly allowance

- Your plan gives each key an allowance of **jobs per month**. See
  [pricing](https://jobs-database.de/pricing) for the allowance of each plan.
- **What counts:** each job returned in a `200` response counts once. A page of 20 jobs costs 20;
  an empty page costs 0; `GET /v1/jobs/{job_id}` costs 1 when found and 0 on `404`. The number of
  jobs *returned* is charged, not the `limit` you asked for.
- **Near the end of the allowance** a page is cut to what is left: with 5 jobs left, `limit=20`
  returns 5 jobs, and `has_more` is `true` if more jobs follow.
- **When it is used up**, every `/v1` request answers `429 {"error": "monthly allowance used up"}`
  — including `GET /v1/jobs/{job_id}` — until the allowance resets.
- **Parallel requests:** while a request is running, the jobs it may return are held for it, and
  a request that ends up returning fewer gives the rest back when it finishes. So with requests
  in flight at the same time, a page can be cut short, or a request can get the allowance `429`,
  even though jobs come free a moment later. If `X-Jobs-Remaining` on your last `200` was above
  `0`, wait for your other requests to finish and retry before treating the month as used up.
- **Reset:** months are calendar months in UTC. The allowance resets on the **1st of each month
  at 00:00 UTC**. Unused jobs do not carry over.

## Response headers

| Header | On | Meaning |
| --- | --- | --- |
| `X-Jobs-Remaining` | `200` responses, keys issued by us | Jobs left in your monthly allowance after this response. Not sent on requests made through RapidAPI — RapidAPI tracks that usage. |
| `Retry-After` | rate-limit `429` | Seconds to wait before the next request (1–60). |
| `Cache-Control: no-store` | every `/v1` response | Responses are not cached by browsers or proxies. |

Use `X-Jobs-Remaining` to track usage: read it after each request and slow down or stop before
it reaches `0`.
