Skip to content

Type an option name like full_page, an error code, or a topic.

Usage and limits

How your monthly quota is counted, how to check what is left, and what the rate limit and concurrency headers on every answer mean.

Ask the API how much of your allowance is left. This call is free.

Request
curl "https://curlshot.com/api/v1/usage" \
  -H "X-Access-Key: YOUR_ACCESS_KEY"
Response, status 200
{
  "plan": { "slug": "free", "name": "Free" },
  "period": { "start": "2026-10-02T23:09:55.000Z", "end": "2026-11-01T23:09:55.000Z" },
  "quota": 100,
  "used": 12,
  "remaining": 88,
  "bonus_remaining": 25,
  "rate_limit_per_minute": 10,
  "concurrency": 1,
  "full_page_max_height": 10000,
  "full_page_max_scale": 1,
  "video_max_seconds": 10
}

The numbers above are a sample. Yours depend on your plan.

FieldMeaning
planThe plan your account is on: a short slug for code and a name for people.
periodThe start and end of the current billing period.
quotaHow many screenshots your plan gives you in this period.
usedHow many of those you have taken so far.
remainingHow many of your plan's screenshots are left: quota minus used.
bonus_remainingExtra screenshots left on top of your plan, for example from referrals.
rate_limit_per_minuteHow many render requests your account may send per minute.
concurrencyHow many renders may run at the same moment.
full_page_max_heightThe tallest full-page screenshot your plan makes, in page pixels.
full_page_max_scaleThe highest pixel density of a full-page screenshot on your plan.
video_max_secondsThe longest video your plan records, in seconds. 0 means the plan has no video capture.

What you can still take in total is remaining plus bonus_remaining. In the sample that is 88 + 25 = 113.

#The three limits

Every plan has three limits. They protect different things, so it helps to keep them apart.

LimitWhat it countsWhen it resetsError when you pass it
QuotaSuccessful renders in the billing periodAt the end of the periodquota_exceeded, status 402
Rate limitRender requests per minute, for the whole accountWithin a minuterate_limited, status 429
ConcurrencyRenders running at the same momentAs soon as a render endsconcurrency_limit, status 429

We do not print the numbers here, because they differ per plan. You find them on the pricing page, and your own values in the GET /usage answer above.

A plan also limits how large one full-page screenshot can be. That is not a fourth counter: a request above the size limit is not refused, the screenshot is made at the limit. A full-page screenshot always counts as one. See Limits of your plan.

#What counts against your quota

The rule is short: one successful render uses one screenshot.

RequestCounted?
A screenshot or PDF that was rendered and returnedYes, 1
A cache hit (X-Cache: HIT)No
A request that failed with any errorNo
Each successful job from an async or bulk callYes, 1 per job
A call to GET /usage, or a status call for a job or a batchNo
A download of a stored file from its urlNo

The size of the screenshot does not matter. A full-page capture of a long article counts the same as a small thumbnail.

On top of your plan's quota you can have bonus screenshots, from things like referrals. The plan's own screenshots are used first. When they are gone, renders keep working until the bonus is used up too.

#Read your limits from any response

You do not have to call /usage to keep track. Every answer to a request with a valid key carries these headers:

Request
curl "https://curlshot.com/api/v1/screenshot?access_key=YOUR_ACCESS_KEY&url=https://example.com" \
  --output example.png -D -

The -D - part tells curl to print the response headers. Here are the ones about limits, for the sample account from the top of this page:

Response headers
HTTP/1.1 200 OK
content-type: image/png
cache-control: private, no-store
x-cache: MISS
x-concurrency-limit: 1
x-concurrency-remaining: 1
x-image-height: 1024
x-image-width: 1280
x-quota-limit: 100
x-quota-remaining: 112
x-quota-reset: 2026-11-01T23:09:55.000Z
x-ratelimit-limit: 10
x-ratelimit-remaining: 9
x-ratelimit-reset: 1791069120
x-reference-id: bcf05dcd0aae40cc84d621758c44e4f8
x-render-ms: 1240
x-request-id: bcf05dcd0aae40cc84d621758c44e4f8

Header names are not case-sensitive. curl prints them in lower case. We write them as X-Quota-Limit in the text because that is easier to read.

HeaderMeaning
X-Reference-IdThe id of this request. Quote it when you contact support. X-Request-Id holds the same value.
X-Quota-LimitYour plan's quota for the current period.
X-Quota-RemainingScreenshots you can still take: what is left of your plan, plus your bonus screenshots.
X-Quota-ResetWhen the period resets, as an ISO date such as 2026-11-01T23:09:55.000Z.
X-RateLimit-LimitRequests allowed per minute.
X-RateLimit-RemainingRequests left in the current minute.
X-RateLimit-ResetWhen the rate limit resets, as Unix seconds (seconds since 1 January 1970).
X-Concurrency-LimitRenders allowed at the same moment.
X-Concurrency-RemainingFree render slots right now.
X-CacheHIT for a stored copy, MISS for a new render.
X-Render-MsHow long the render took, in milliseconds.
X-Image-Width, X-Image-HeightThe size of the screenshot in pixels. Only on image answers.
Retry-AfterOnly on status 429 and 503. How many seconds to wait before you try again.

X-Quota-Remaining includes your bonus screenshots, so it can be larger than X-Quota-Limit. In the sample, 87 are left of the plan and 25 of the bonus, which makes 112.

Note the two date styles. X-Quota-Reset is a readable date. X-RateLimit-Reset is a plain number of seconds.

#Status calls have their own rate limit

Some calls render nothing. They only report on something:

  • GET https://curlshot.com/api/v1/usage
  • GET https://curlshot.com/api/v1/jobs/:id, the status of an async job
  • GET https://curlshot.com/api/v1/batches/:id, the status of a bulk batch
  • GET https://curlshot.com/api/v1/files/:id with an access key, a download of a stored file

These are free, and they are counted in a separate bucket. So checking on your jobs never uses up the requests you need for renders.

The bucket allows 10 times your plan's per-minute limit, and never less than 300 per minute. On a plan with 10 requests per minute, that is 300 status calls per minute. On a plan with 100, it is 1000.

On the answers to these calls, the three X-RateLimit-* headers describe this bucket, not your render limit:

Response headers of GET /usage
HTTP/1.1 200 OK
content-type: application/json; charset=utf-8
x-ratelimit-limit: 300
x-ratelimit-remaining: 299
x-ratelimit-reset: 1791069120

Your render limit is always in the body of GET /usage, as rate_limit_per_minute.

A stored file opened through its own url, the link with a token in it, is not counted at all.

#When the quota runs out

When remaining and bonus_remaining are both zero, the next render is refused:

Response, status 402
{
  "error_code": "quota_exceeded",
  "error_message": "The screenshot quota for this billing period is used up.",
  "documentation_url": "https://curlshot.com/docs/errors#quota_exceeded"
}

Retrying does not help here. The quota comes back when the period resets, at the time in X-Quota-Reset. To continue sooner, move to a larger plan on the pricing page.

#When you send requests too fast

Send more requests in a minute than your plan allows, and you get this:

Response headers, status 429
HTTP/1.1 429 Too Many Requests
content-type: application/json; charset=utf-8
retry-after: 12
x-ratelimit-limit: 10
x-ratelimit-remaining: 0
x-ratelimit-reset: 1791069120
Response body
{
  "error_code": "rate_limited",
  "error_message": "Too many requests. Slow down and retry after the indicated delay.",
  "documentation_url": "https://curlshot.com/docs/errors#rate_limited"
}

You get the same status 429 with the code concurrency_limit when too many of your renders are running at once.

Neither one is a real failure. Nothing was rendered and nothing was counted. Wait, then send the request again.

#Fair use limits

The renderers are shared by all customers. Three more rules keep one account from slowing down the others. Most accounts never notice them.

RuleWhat it countsThe numberError when you pass it
Bulk weightA bulk call counts as one request per item against your rate limit.Your rate limitrate_limited, status 429
QueueAsync and bulk jobs of your account that are waiting or rendering at the same time.100 for each render of your concurrency, and never less than 200queue_limit, status 429
Failed rendersRenders of your account that failed within the last minute.Half your rate limit per minute, and never less than 5failure_limit, status 429

So with rate_limit_per_minute 60 and concurrency 5 from GET /usage, your queue holds 500 jobs and 30 renders may fail per minute. A plan can set its own numbers.

#A bulk call counts per item

A batch of 25 uses 25 requests of the current minute. When fewer are left, the call is refused and nothing is queued. A batch larger than your whole per-minute limit is accepted only when you have sent nothing else for a minute or two, and it uses the whole minute.

#The queue has a size

Jobs you start with async=true or with a bulk call wait in a queue. When the queue of your account is full, new jobs are refused:

Response, status 429
{
  "error_code": "queue_limit",
  "error_message": "This account already has 500 jobs waiting to render; your plan allows 500 at a time. Nothing was queued. Wait for some to finish, then retry.",
  "documentation_url": "https://curlshot.com/docs/errors#queue_limit"
}

A job leaves the queue when it is done or has failed. Sync requests, the ones that return the screenshot directly, are not jobs and do not count.

Two more things hold for jobs:

  • Your jobs render at most concurrency at once, across all our renderers. This is separate from the slots of your sync requests.
  • The queue serves accounts in turn: everyone's first waiting job, then everyone's second, and so on. A large batch from another customer does not delay your single job.

#Failed renders are free, up to a point

A failed render costs you nothing, but it used a browser on our side. So the number of renders that may fail per minute is capped. What counts is a render that started and did not finish: a timeout, a page that could not be loaded, a selector that was not found, a request you cancelled half-way. A request that is refused before anything renders, such as invalid_options, does not count. Neither does a problem on our side.

When the cap is reached, new renders are refused for the rest of that minute:

Response, status 429
{
  "error_code": "failure_limit",
  "error_message": "Too many renders of this account failed in the last minute (the limit on your plan is 30). Check the failing requests, then retry after the indicated delay.",
  "documentation_url": "https://curlshot.com/docs/errors#failure_limit"
}

The Retry-After header says how long to wait. Cache hits and status calls are still answered, because they use no browser. Jobs that are already queued are not thrown away: they wait until the minute has passed, then continue.

If you see this error, something in your requests fails again and again. Fix that first. Waiting alone brings the same error back.

#Back off with Retry-After

Backing off means waiting before you retry, so you do not hit the limit again right away. The Retry-After header tells you how many seconds to wait.

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

async function fetchWithBackoff(url, tries = 5) {
  for (let attempt = 1; attempt <= tries; attempt++) {
    const response = await fetch(url)
    if (response.status !== 429) return response

    // Wait as long as the API asks. Fall back to a growing pause if the header is missing.
    const seconds = Number(response.headers.get('retry-after')) || attempt * 2
    await sleep(seconds * 1000)
  }
  throw new Error('Still rate limited after several tries')
}

A fuller version, which also retries temporary render errors, is on the errors page.

#Where to go next

  • Errors: every error code, and which ones are safe to retry.
  • Caching: the simplest way to use fewer screenshots.
  • Bulk screenshots: start many renders with one request.
  • Pricing: the quota, rate limit and concurrency of each plan.