# Errors

> Every error the Grabber API returns, with its HTTP status, its stable detail code and what to do next. Errors are never charged.

Every error has the shape `{ status, message, detail }`, plus a `hint` when
there is something the caller can do about it. `detail` is a stable code to
branch on; `message` is the HTTP reason phrase. Nothing is ever cached:
every answer has `Cache-Control: no-store`.

You pay only for a page that was loaded, so no error is ever charged. A page whose own server answered 404 or 500 is not an error: it was loaded, so the answer is `200` and it is charged. Its status is in `data.fetch.httpStatusCode`.

## Error codes

| Status | detail | Meaning |
| --- | --- | --- |
| `400` | `invalid_query` | The query is wrong, and `hint` says how. |
| `400` | `invalid_url` | The url is not `http` or `https`, has a username or password, or is too long. |
| `400` | `blocked_url` | The url points at a host that cannot be fetched. |
| `401` | `invalid_api_key` | The key is missing, malformed, unknown, or deleted. |
| `402` | `insufficient_credits` | The balance is below the price of this request. Nothing was charged and nothing was fetched. |
| `404` | `not_found` | The path is not `/fetch`. |
| `405` | `method_not_allowed` | The method is not `GET`. |
| `429` | `rate_limited` | More than 60 requests in this minute for this key. Wait `Retry-After` seconds. |
| `500` | `internal_error` | Something failed on Grabber's side. Nothing was charged. Try again; if it keeps happening, it is not caused by your request. |
| `502` | `fetch_failed` | The page could not be loaded. |
| `502` | `blocked_url` | The page redirected to a host that cannot be fetched. |
| `502` | `body_too_large` | The page is over 8 MiB. |
| `502` | `format_unavailable` | `unwrap=true`, and none of the requested formats can be produced for this content. |
| `502` | `upstream_unavailable` | Grabber could not get an answer from its fetching service. |

## Examples

### No url, or more than one

```json
{
  "status": 400,
  "message": "Bad Request",
  "detail": "invalid_query",
  "hint": "Give exactly one address to fetch, for example ?url=https://example.com/"
}
```

### A parameter that does not exist

```json
{
  "status": 400,
  "message": "Bad Request",
  "detail": "invalid_query",
  "hint": "Unknown parameter \"key\". If it is part of the address to fetch, percent-encode the whole url value."
}
```

### A format that does not exist

```json
{
  "status": 400,
  "message": "Bad Request",
  "detail": "invalid_query",
  "hint": "One of the values in formats is not supported. Use markdown, html, text, links or raw."
}
```

### No credit

```json
{
  "status": 402,
  "message": "Payment Required",
  "detail": "insufficient_credits",
  "hint": "Add credits at https://dashboard.grabber.sh/billing"
}
```

### Rate limited

```json
{
  "status": 429,
  "message": "Too Many Requests",
  "detail": "rate_limited"
}
```

## What to do

- **400**: the request itself is wrong, so sending it again gives the same answer. Read `hint`, fix the query or the URL, then retry.
- **401**: check that the key is sent as `Authorization: Bearer gsh_…` and still exists in the dashboard.
- **402**: add credit at the address in `hint`, or turn on auto top-up so the balance never runs out.
- **429**: wait the number of seconds in `Retry-After`, then retry. The limit is 60 requests per minute per key.
- **500** and **502 `upstream_unavailable`**: the problem is on Grabber's side. Retry with a growing delay, such as 1, 2, then 4 seconds.
- **502 `fetch_failed`**: the page did not load. Check the URL in a browser; if it opens there, try again later.
- **502 `format_unavailable`**: with `unwrap=true`, add a format this content can produce, such as `raw`, after the one you want.

## Response headers

| Header | Meaning |
| --- | --- |
| `X-Grabber-Cost` | What this request cost, in US dollars: `0.001`, `0.005`, or `0` when nothing was charged. |
| `X-Grabber-Balance` | What is left of your credit after this request, in US dollars. |
| `Cache-Control` | Always `no-store`. Answers are never cached. |
| `WWW-Authenticate` | Always `Bearer`. |
| `Allow` | Always `GET`. |
| `Retry-After` | Seconds until the next minute begins. |
