# Errors

What a failed request looks like, which errors are safe to retry, and the cause and fix for every error code.

Here is a request that fails. The `url` has no `https://` in front.

```bash
curl "https://curlshot.com/api/v1/screenshot?access_key=YOUR_ACCESS_KEY&url=example.com"
```

```json
{
  "error_code": "invalid_options",
  "error_message": "url: must be a full URL starting with http:// or https://",
  "documentation_url": "https://curlshot.com/docs/errors#invalid_options",
  "errors": [
    { "field": "url", "message": "must be a full URL starting with http:// or https://" }
  ]
}
```

You never get a broken image. When something goes wrong, you get a short JSON message that says what happened and where to read more.

## The shape of an error

Every error has the same three fields. One code, `invalid_options`, adds a fourth.

| Field | Meaning |
| --- | --- |
| `error_code` | A fixed word for the kind of problem, such as `invalid_options`. Use this one in your code. |
| `error_message` | A sentence for humans. It can change over time, and for some errors it holds details. Do not compare against it in code. |
| `documentation_url` | A link to the section of this page that explains the code. |
| `errors` | Only with [`invalid_options`](#invalid_options). A list with one entry for each option that is wrong. |

The HTTP status tells you the family of the problem. The `error_code` tells you exactly which one.

### The errors list

Each entry in `errors` names one problem, so your code can point at the exact option.

| Field | Meaning |
| --- | --- |
| `field` | The name of the option that is wrong, such as `url`. |
| `message` | What is wrong with it. |
| `index` | Only in a [bulk](https://curlshot.com/docs/bulk.md#when-an-item-is-wrong) call. The position of the item in your `requests` list, counted from `0`. |

An option the API does not know is refused too, so a typo never passes silently. The message suggests the right name:

```bash
curl "https://curlshot.com/api/v1/screenshot?access_key=YOUR_ACCESS_KEY&url=https://example.com&fullpage=true"
```

```json
{
  "error_code": "invalid_options",
  "error_message": "fullpage: is not a known option; did you mean `full_page`?",
  "documentation_url": "https://curlshot.com/docs/errors#invalid_options",
  "errors": [
    { "field": "fullpage", "message": "is not a known option; did you mean `full_page`?" }
  ]
}
```

## How to tell success from failure

Check two things before you save a response as a file:

1. The HTTP status is `200`.
2. The `Content-Type` header starts with `image/` or `video/`, or is `application/pdf`.

An error always has a status of `400` or higher and the content type `application/json`.

Every answer also carries an `X-Reference-Id` header. If you [write to us](https://curlshot.com/contact) about a failed request, include that id. It lets us find the exact render.

Failed requests are free. Only a successful render uses a screenshot from your [quota](https://curlshot.com/docs/usage-and-limits.md).

## Which errors to retry

Some errors are about your request. Sending the same request again gives the same error. Others are about a moment in time, and a second try often works.

| Status | What it means | Retry? |
| --- | --- | --- |
| `400`, `413`, `422` | Something in the request is wrong. | No. Fix the request first. |
| `401`, `403` | The key or the signature is missing or wrong, the link has expired, the host is not allowed, or your plan does not include the feature. | No. Fix the key, the signature, the link or the address. |
| `402` | Your quota is used up. | No. Wait for the new period or upgrade. |
| `404` | The job or batch id does not exist for your account ([`job_not_found`](#job_not_found)), a stored file is gone or its link has expired ([`file_not_found`](#file_not_found)), or the path is not an API endpoint ([`not_found`](#not_found)). | No. Check the id or the path, or render the page again. |
| `405` | The endpoint does not take that HTTP method ([`method_not_allowed`](#method_not_allowed)). The `Allow` header lists the ones it takes. | No. Use a method from `Allow`. |
| `429` | You are sending too fast, too many renders are running, too many jobs are waiting, or too many renders failed in the last minute. | Yes, after the wait in `Retry-After`. |
| `500`, `502`, `503`, `504` | A temporary problem on our side or on the target site. | Yes, with a short pause. |

The table of codes below has a "Retry?" column. It answers the question "does it help to send the same request again right away?"

The four `429` codes, `rate_limited`, `concurrency_limit`, `queue_limit` and `failure_limit`, are marked "after waiting" there, because an instant retry fails again. They do succeed once you have waited. The `Retry-After` header says how many seconds.

Every answer from an address under `https://curlshot.com/api/v1` is JSON in this shape, also for a path that does not exist or a method an endpoint does not take:

```bash
curl -X DELETE "https://curlshot.com/api/v1/screenshot"
```

```json
{
  "error_code": "method_not_allowed",
  "error_message": "This endpoint does not accept that HTTP method. Use GET or POST.",
  "documentation_url": "https://curlshot.com/docs/errors#method_not_allowed"
}
```

| Code | Status | Retry? |
| --- | --- | --- |
| [`invalid_url`](https://curlshot.com/docs/errors.md#invalid_url) | 400 | no |
| [`host_not_allowed`](https://curlshot.com/docs/errors.md#host_not_allowed) | 403 | no |
| [`navigation_failed`](https://curlshot.com/docs/errors.md#navigation_failed) | 502 | yes |
| [`timeout`](https://curlshot.com/docs/errors.md#timeout) | 504 | yes |
| [`selector_not_found`](https://curlshot.com/docs/errors.md#selector_not_found) | 422 | no |
| [`concurrency_limit`](https://curlshot.com/docs/errors.md#concurrency_limit) | 429 | after waiting |
| [`content_too_large`](https://curlshot.com/docs/errors.md#content_too_large) | 413 | no |
| [`internal_error`](https://curlshot.com/docs/errors.md#internal_error) | 500 | yes |
| [`renderer_busy`](https://curlshot.com/docs/errors.md#renderer_busy) | 503 | yes |
| [`renderer_unavailable`](https://curlshot.com/docs/errors.md#renderer_unavailable) | 503 | yes |
| [`invalid_options`](https://curlshot.com/docs/errors.md#invalid_options) | 400 | no |
| [`access_key_required`](https://curlshot.com/docs/errors.md#access_key_required) | 401 | no |
| [`access_key_invalid`](https://curlshot.com/docs/errors.md#access_key_invalid) | 401 | no |
| [`signature_required`](https://curlshot.com/docs/errors.md#signature_required) | 403 | no |
| [`signature_invalid`](https://curlshot.com/docs/errors.md#signature_invalid) | 403 | no |
| [`quota_exceeded`](https://curlshot.com/docs/errors.md#quota_exceeded) | 402 | no |
| [`rate_limited`](https://curlshot.com/docs/errors.md#rate_limited) | 429 | after waiting |
| [`job_not_found`](https://curlshot.com/docs/errors.md#job_not_found) | 404 | no |
| [`service_unavailable`](https://curlshot.com/docs/errors.md#service_unavailable) | 503 | yes |
| [`invalid_request`](https://curlshot.com/docs/errors.md#invalid_request) | 400 | no |
| [`email_not_verified`](https://curlshot.com/docs/errors.md#email_not_verified) | 403 | no |
| [`feature_not_available`](https://curlshot.com/docs/errors.md#feature_not_available) | 403 | no |
| [`request_expired`](https://curlshot.com/docs/errors.md#request_expired) | 403 | no |
| [`not_found`](https://curlshot.com/docs/errors.md#not_found) | 404 | no |
| [`file_not_found`](https://curlshot.com/docs/errors.md#file_not_found) | 404 | no |
| [`method_not_allowed`](https://curlshot.com/docs/errors.md#method_not_allowed) | 405 | no |
| [`queue_limit`](https://curlshot.com/docs/errors.md#queue_limit) | 429 | after waiting |
| [`failure_limit`](https://curlshot.com/docs/errors.md#failure_limit) | 429 | after waiting |

## Every error code

### invalid_url

HTTP status `400`. Retrying the same request will not help.

> The URL is not valid. Use a full http:// or https:// address.

**Why it happens.** The `url` is missing its scheme, has a typo, or was not URL-encoded so part of it got cut off.

**How to fix it.** Send a full address starting with `http://` or `https://`, and [encode it](https://curlshot.com/docs/screenshot-url.md#encode-the-url) when it goes in a query string.

### host_not_allowed

HTTP status `403`. Retrying the same request will not help.

> This host cannot be rendered. Private, local and internal network addresses are blocked.

**Why it happens.** The address points at a private or local network, such as `localhost`, `192.168.x.x` or a cloud metadata address.

**How to fix it.** Use a page that is reachable from the public internet. For a local site, expose it with a tunnel or send the [HTML](https://curlshot.com/docs/html-and-markdown.md) directly.

### navigation_failed

HTTP status `502`. Safe to retry after a short pause.

> The page could not be loaded. Check that the address is correct and publicly reachable.

**Why it happens.** The site did not answer: the domain does not exist, the server is down, or it refused the connection.

**How to fix it.** Open the address in your own browser to check it. If it works there, retry; the failure may have been temporary.

### timeout

HTTP status `504`. Safe to retry after a short pause.

> The page took too long to render. Try a larger `timeout` or a lighter `wait_until`.

**Why it happens.** The page was not ready before the `timeout` ran out. Heavy pages and `wait_until=networkidle` are the usual reasons.

**How to fix it.** Raise [`timeout`](https://curlshot.com/docs/options.md#timeout), switch to `wait_until=load`, or wait for one element with [`wait_for_selector`](https://curlshot.com/docs/waiting.md).

### selector_not_found

HTTP status `422`. Retrying the same request will not help.

> No element on the page matches the selector.

**Why it happens.** Nothing on the page matched `selector`, `click` or `wait_for_selector` in time.

**How to fix it.** Check the selector in your browser console with `document.querySelector(...)`. If the element appears late, add a [`delay`](https://curlshot.com/docs/waiting.md).

### concurrency_limit

HTTP status `429`. Wait, then retry: the same request works once the limit frees up.

> Too many renders are running at once for this account. Retry in a moment.

**Why it happens.** Your account already has as many renders running as your plan allows at the same time.

**How to fix it.** Wait for a render to finish and retry, queue the work with [`async`](https://curlshot.com/docs/async-and-webhooks.md), or move to a plan with more concurrency.

### content_too_large

HTTP status `413`. Retrying the same request will not help.

> The request or the rendered output is larger than the allowed size.

**Why it happens.** The `html` or `markdown` you sent, or the image that came out, is bigger than the size limit.

**How to fix it.** Send less HTML, lower [`full_page_max_height`](https://curlshot.com/docs/options.md#full_page_max_height), or use `format=jpeg` to shrink the output.

### internal_error

HTTP status `500`. Safe to retry after a short pause.

> Something went wrong while rendering. The render was not billed; please retry.

**Why it happens.** Something broke on our side while rendering.

**How to fix it.** Retry the request. It was not counted against your quota. If it keeps happening, send us the reference id from the response headers.

### renderer_busy

HTTP status `503`. Safe to retry after a short pause.

> All render slots are busy right now. Retry in a moment.

**Why it happens.** Every render slot was taken at that moment.

**How to fix it.** Retry after a second or two. Retrying with a short pause is always safe.

### renderer_unavailable

HTTP status `503`. Safe to retry after a short pause.

> The rendering service is temporarily unavailable. Retry in a moment.

**Why it happens.** The rendering service could not be reached.

**How to fix it.** Retry after a few seconds. Nothing was counted against your quota.

### invalid_options

HTTP status `400`. Retrying the same request will not help.

> One or more options are not valid.

**Why it happens.** An option has a value it cannot take, is misspelled, or two options conflict (for example `selector` with `clip_width`).

**How to fix it.** Read `error_message`: it names each option and what is wrong with it. The [options reference](https://curlshot.com/docs/options.md) lists every allowed value.

### access_key_required

HTTP status `401`. Retrying the same request will not help.

> An access key is required. Pass `access_key` or the `X-Access-Key` header.

**Why it happens.** The request had no access key.

**How to fix it.** Add `access_key=YOUR_ACCESS_KEY` to the query string or send the `X-Access-Key` header. See [Authentication](https://curlshot.com/docs/authentication.md).

### access_key_invalid

HTTP status `401`. Retrying the same request will not help.

> The access key is not valid or has been revoked.

**Why it happens.** The key does not exist, was mistyped, or has been revoked.

**How to fix it.** Copy the key again from your dashboard. If it was revoked, create a new one.

### signature_required

HTTP status `403`. Retrying the same request will not help.

> This access key only accepts signed requests. Add a `signature` parameter.

**Why it happens.** This key is set to accept signed requests only, and the request had no `signature`.

**How to fix it.** Sign the request as shown in [Signed links](https://curlshot.com/docs/signed-links.md), or turn the setting off for this key.

### signature_invalid

HTTP status `403`. Retrying the same request will not help.

> The request signature does not match.

**Why it happens.** The `signature` does not match the parameters. Usually a parameter changed after signing, or the wrong secret was used.

**How to fix it.** Sign the exact query string you send, with the secret that belongs to the same access key. See [Signed links](https://curlshot.com/docs/signed-links.md#common-mistakes).

### quota_exceeded

HTTP status `402`. Retrying the same request will not help.

> The screenshot quota for this billing period is used up.

**Why it happens.** You have used every screenshot included in this billing period.

**How to fix it.** Wait for the period to reset, or upgrade your plan. [Usage and limits](https://curlshot.com/docs/usage-and-limits.md) shows how to check what is left.

### rate_limited

HTTP status `429`. Wait, then retry: the same request works once the limit frees up.

> Too many requests. Slow down and retry after the indicated delay.

**Why it happens.** You sent more requests per minute than your plan allows.

**How to fix it.** Wait the number of seconds in the `Retry-After` header, then continue. Spread requests out or use [bulk](https://curlshot.com/docs/bulk.md).

### job_not_found

HTTP status `404`. Retrying the same request will not help.

> No job with this id exists for your account.

**Why it happens.** The job id does not exist, or it belongs to another account.

**How to fix it.** Use the `job_id` exactly as it was returned, with the same access key that created it.

### service_unavailable

HTTP status `503`. Safe to retry after a short pause.

> The service is temporarily unavailable. Retry in a moment.

**Why it happens.** A part of the service that checks your limits is temporarily down, so the request was refused rather than let through uncounted.

**How to fix it.** Retry after a few seconds.

### invalid_request

HTTP status `400`. Retrying the same request will not help.

> The request could not be read. Send a JSON object.

**Why it happens.** The body of a `POST` request could not be read: it is not valid JSON, it is not a JSON object, or a bulk call has no `requests` list or too many items.

**How to fix it.** Send a JSON object with the header `Content-Type: application/json`. For bulk, see [the request format](https://curlshot.com/docs/bulk.md).

### email_not_verified

HTTP status `403`. Retrying the same request will not help.

> The email address of this account is not verified yet. Open the verification link we emailed you, then retry.

**Why it happens.** The key is valid, but the email address of its account is not verified. This happens right after you change the account email, or when you signed up through a provider that did not confirm the address.

**How to fix it.** Open the verification link we emailed you. The key works again as soon as the address is verified; you can request a new link from the sign-in page.

### feature_not_available

HTTP status `403`. Retrying the same request will not help.

> Your plan does not include this feature.

**Why it happens.** The request uses something your plan does not include, such as PDF output, `async`, bulk or the response cache.

**How to fix it.** Read `error_message`: it names the feature. Remove that option, or move to a plan that includes it.

### request_expired

HTTP status `403`. Retrying the same request will not help.

> This request has expired: its `expires` time is in the past. Create a new link with a later `expires`.

**Why it happens.** The request carries an `expires` time that is already in the past. Usually an old signed link.

**How to fix it.** Create a new link with a later `expires`. A value that is not a Unix time in whole seconds is a different error, `invalid_options`. See [Signed links](https://curlshot.com/docs/signed-links.md).

### not_found

HTTP status `404`. Retrying the same request will not help.

> Nothing was found at this address.

**Why it happens.** The address is not part of the API. Usually a typo in the path.

**How to fix it.** Check the path against [The screenshot URL](https://curlshot.com/docs/screenshot-url.md). Every endpoint starts with `/api/v1/`.

### file_not_found

HTTP status `404`. Retrying the same request will not help.

> This file does not exist or has expired.

**Why it happens.** The stored file was deleted when it expired, the link is cut off or was changed, or the file belongs to another account.

**How to fix it.** Use the `url` exactly as it was returned, before its `expires_at` time. After that, take the screenshot again.

### method_not_allowed

HTTP status `405`. Retrying the same request will not help.

> This endpoint does not accept that HTTP method.

**Why it happens.** The endpoint does not take that HTTP method, for example `DELETE` on `/screenshot`.

**How to fix it.** Use `GET` or `POST` for screenshots, `POST` for bulk, and `GET` for jobs, batches, files and usage.

### queue_limit

HTTP status `429`. Wait, then retry: the same request works once the limit frees up.

> Too many async jobs of this account are waiting to render. Wait for some to finish, then retry.

**Why it happens.** The account already has as many async and bulk jobs waiting or rendering as its plan allows, so the new ones were not queued.

**How to fix it.** Wait for some jobs to finish, then send the call again. Nothing was counted. See [Usage and limits](https://curlshot.com/docs/usage-and-limits.md#fair-use-limits).

### failure_limit

HTTP status `429`. Wait, then retry: the same request works once the limit frees up.

> Too many renders of this account failed in the last minute. Fix the failing requests, then retry after the indicated delay.

**Why it happens.** Too many renders of this account failed within the last minute. Failed renders are free, so their number per minute is capped.

**How to fix it.** Look at the failing requests first: a wrong selector, a page that times out, an address that cannot be reached. Then wait the seconds in `Retry-After`. See [Usage and limits](https://curlshot.com/docs/usage-and-limits.md#fair-use-limits).

## Retry with backoff

Backoff means that you wait a little longer before each new try. It gives a busy service time to recover, and it keeps you inside your rate limit.

This function retries the codes that are worth retrying, and gives up on the rest at once.

```javascript
const RETRYABLE = new Set([
  'renderer_busy',
  'renderer_unavailable',
  'service_unavailable',
  'internal_error',
  'timeout',
  'navigation_failed',
  'rate_limited',
  'concurrency_limit',
  'queue_limit',
  'failure_limit',
])

const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms))

async function takeScreenshot(params, tries = 4) {
  const query = new URLSearchParams({ access_key: 'YOUR_ACCESS_KEY', ...params })

  for (let attempt = 1; ; attempt++) {
    const response = await fetch(`https://curlshot.com/api/v1/screenshot?${query}`)
    if (response.ok) return Buffer.from(await response.arrayBuffer())

    const error = await response.json()
    if (!RETRYABLE.has(error.error_code) || attempt === tries) {
      throw new Error(`${error.error_code}: ${error.error_message}`)
    }

    // Use Retry-After when the API sends it. Otherwise wait 1, 2, 4... seconds.
    const seconds = Number(response.headers.get('retry-after')) || 2 ** (attempt - 1)
    await sleep(seconds * 1000)
  }
}

const image = await takeScreenshot({ url: 'https://example.com' })
```

The same pattern in other languages is on the code example pages: [cURL](https://curlshot.com/docs/examples/curl.md), [Node.js](https://curlshot.com/docs/examples/node.md), [Python](https://curlshot.com/docs/examples/python.md), [PHP](https://curlshot.com/docs/examples/php.md), [Go](https://curlshot.com/docs/examples/go.md) and [Ruby](https://curlshot.com/docs/examples/ruby.md).

> **Tip**
>
> Set a limit on the number of tries. Three or four is plenty. If a page still times out after that, the fix is in the options, not in more retries. See [Waiting and timing](https://curlshot.com/docs/waiting.md).

> **Common mistakes**
>
> - **You saved the error as a screenshot.** If a file will not open, look inside it with a text editor. It probably holds the JSON error. Check the status before you save.
> - **You compare against `error_message`.** The wording can change. Compare against `error_code`.
> - **You retry everything.** Retrying `invalid_options` or `quota_exceeded` in a loop only burns your rate limit.
> - **You expect `invalid_url` for an address without `https://`.** A value that fails the option rules is reported as `invalid_options`, with the option named in `errors`.
> - **You retry a `timeout` with the same options, forever.** One or two retries are fine. After that, raise [`timeout`](https://curlshot.com/docs/options.md#timeout) or change [`wait_until`](https://curlshot.com/docs/options.md#wait_until).
> - **You look for errors only in the first answer of an async job.** With [`async=true`](https://curlshot.com/docs/async-and-webhooks.md), render errors show up later, in the job's `error_code`.

## Where to go next

- [Usage and limits](https://curlshot.com/docs/usage-and-limits.md): the quota, rate limit and concurrency behind the `402` and `429` answers.
- [Waiting and timing](https://curlshot.com/docs/waiting.md): how to avoid `timeout` on slow pages.
- [Signed links](https://curlshot.com/docs/signed-links.md#common-mistakes): the usual causes of `signature_invalid`.
- [Options reference](https://curlshot.com/docs/options.md): every allowed value, for when you get `invalid_options`.
