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.
curl "https://curlshot.com/api/v1/screenshot?access_key=YOUR_ACCESS_KEY&url=example.com"{
"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. 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 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:
curl "https://curlshot.com/api/v1/screenshot?access_key=YOUR_ACCESS_KEY&url=https://example.com&fullpage=true"{
"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:
- The HTTP status is
200. - The
Content-Typeheader starts withimage/orvideo/, or isapplication/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 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.
#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), a stored file is gone or its link has expired (file_not_found), or the path is not an API endpoint (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). 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:
curl -X DELETE "https://curlshot.com/api/v1/screenshot"{
"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 | 400 | no |
host_not_allowed | 403 | no |
navigation_failed | 502 | yes |
timeout | 504 | yes |
selector_not_found | 422 | no |
concurrency_limit | 429 | after waiting |
content_too_large | 413 | no |
internal_error | 500 | yes |
renderer_busy | 503 | yes |
renderer_unavailable | 503 | yes |
invalid_options | 400 | no |
access_key_required | 401 | no |
access_key_invalid | 401 | no |
signature_required | 403 | no |
signature_invalid | 403 | no |
quota_exceeded | 402 | no |
rate_limited | 429 | after waiting |
job_not_found | 404 | no |
service_unavailable | 503 | yes |
invalid_request | 400 | no |
email_not_verified | 403 | no |
feature_not_available | 403 | no |
request_expired | 403 | no |
not_found | 404 | no |
file_not_found | 404 | no |
method_not_allowed | 405 | no |
queue_limit | 429 | after waiting |
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 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 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
timeoutor a lighterwait_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, switch to wait_until=load, or wait for one element with wait_for_selector.
#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.
#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, 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, 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 lists every allowed value.
#access_key_required
HTTP status 401. Retrying the same request will not help.
An access key is required. Pass
access_keyor theX-Access-Keyheader.
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.
#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
signatureparameter.
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, 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.
#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 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.
#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.
#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
expirestime is in the past. Create a new link with a laterexpires.
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.
#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. 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.
#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.
#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.
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, Node.js, Python, PHP, Go and Ruby.
#Where to go next
- Usage and limits: the quota, rate limit and concurrency behind the
402and429answers. - Waiting and timing: how to avoid
timeouton slow pages. - Signed links: the usual causes of
signature_invalid. - Options reference: every allowed value, for when you get
invalid_options.