Rate limits

The ScreenshotBuddy API limits how fast an account may ask for captures. There are two limits rather than one, because the two kinds of answer cost us completely different things: a render is a browser and a page load, while a repeat we can serve out of your cache is a file read. This page explains what each of them is, how to read them off a response, and what happens when you go over one.

Your limits

An account may ask for up to 20 renders per minute by default. Larger plans carry a higher limit, and an account can be given a limit of its own, so treat 20 as the floor rather than the number your integration should hard-code.

Answers served out of your cache are counted apart from that, against a flat 300 per minute. That number is the same for every account whatever plan it is on, because a hit costs a file read rather than a render and there is nothing in it for a plan to scale. It is sized for the case it exists for: a page of signed URL thumbnails fetches twenty images the moment it loads, every one of them a hit. Charged to the render limit, that single page load would spend a whole minute of a plan's renders.

What spends which

A request spends a render when it asks us to render: a capture we have not made yet, or one sent with cache=0. A repeat we answer out of the cache spends a cached answer instead. A conditional request we can answer 304 out of your cache spends neither, so asking whether a capture you already hold has changed stays free in every sense.

A 304 we had to render to establish is the other case. The page was loaded before we could say it looks the same, so it spends a render exactly like the 200 it replaced, and it carries the render pair to say so. What the answer saved you is the download.

A request we refuse before we get as far as the cache costs you nothing at all. Failing validation, asking for a mode your token was narrowed away from, or sending from an account whose email is not verified leaves both budgets exactly where they were.

Being refused for having no plan, or for having spent every credit, does spend a render. That request did ask us for render capacity, and a refusal that costs nothing to ask for is one that can be asked for in a loop.

Reading your limit off a response

Rather than keeping count yourself, read the headers the API sends back.

  • Name
    X-RateLimit-Limit
    Type
    integer
    Description

    The size of the budget this answer was counted against: your own render limit when the page was rendered for you, the flat 300 when the answer came out of the cache. The X-Cache header on the same answer says which of the two it was.

  • Name
    X-RateLimit-Remaining
    Type
    integer
    Description

    What is left of that same budget after the request you are reading it on. When it reaches 0, hold off until the minute is over.

  • Name
    Retry-After
    Type
    integer
    Description

    Sent with a 429, and with the other statuses worth retrying. It is the number of seconds to wait before sending the request again.

  • Name
    X-Credits-Limit
    Type
    integer
    Description

    The credits your current period was granted. Sent with every answer to a request that carried a working token, a rendered file included.

  • Name
    X-Credits-Remaining
    Type
    integer
    Description

    The credits you have left, counted after the request you are reading it on. A render that worked is already subtracted, and one that failed is already refunded, so you never have to guess which of the two happened.

  • Name
    X-Credits-Reset
    Type
    integer
    Description

    When your credits are granted again, as a Unix timestamp in seconds.

  • Name
    X-Request-Id
    Type
    string
    Description

    Identifies that one answer. It is on every response we send, so you can log it alongside a rendered file as well as alongside an error.

  • Name
    X-Target-Status
    Type
    integer
    Description

    The status the page you captured answered, which is not the status of this response: a rendered 404 page comes back as a 200 carrying X-Target-Status: 404. On screenshot answers only, and absent when we were not told one. Absent means not known rather than 200. See what the target answered.

The two sets answer two different questions. Credits are how many you have; the rate limit is how fast you may spend them. Running out of credits is not fixed by waiting a minute, and hitting the rate limit costs you nothing.

The rate limit pair is on a rendered answer and on a 429, and nowhere else. A 304 we answered out of your cache carries neither, because it was counted against neither budget, and a refusal we answer before anything is counted has no budget to report. A 304 we had to render for does carry the pair, because it was counted. On a 429 the pair describes the limit that actually refused the request, next to the Retry-After that says how long it holds, so you can tell which of the two you ran into.

The headers only arrive with an answer, so they tell you where you stand after you have spent a request. To find that out before you send one, ask the usage endpoint.

Both sets are on answers to a request that carried your bearer token, and on none of the answers to a signed URL. What a signed request escapes is the reporting, not the limit. Every fetch of a signed URL is counted in full, against both budgets and against the credits of the account whose token signed it, exactly as the same request sent with that token as a bearer would be, and it is refused with a 429 when it goes over. What a signed answer never carries is the numbers.

A signed URL is built to be handed out, so its answer is read by whoever you gave it to and cached by whatever sits in front of them: your plan size and how much of it you have spent are not something an embedded image should be telling its readers. X-Request-Id and Retry-After are on both, because neither says anything about your account, so a signed 429 still tells whoever fetched it how long to wait. To see where a signed URL left your account, read the headers off a bearer request or ask the usage endpoint.

Checking your usage

A GET to /v1/usage reports where your account stands: the credits you have left, used and were granted this period, the plan you are on, the date the period resets, and both of the per-minute limits your account is actually held to, as requests_per_minute and cached_requests_per_minute.

It describes your account rather than refusing on it. An account with no plan, or one that has spent every credit, is answered 200 with the numbers that say so, where a render would come back as 402.

It carries a limit of its own of 60 requests per minute, separate from both limits above. Checking your usage therefore never spends anything you were saving for a render, so you are free to ask before every batch.

GET
/v1/usage
curl "https://api.screenshotbuddy.io/v1/usage" \
  -H "Authorization: Bearer {token}"
200
application/json
{
    "credits": {
        "remaining": 8432,
        "used": 1568,
        "total": 10000
    },
    "plan": {
        "name": "Business",
        "slug": "business"
    },
    "period": {
        "started_at": "2026-08-01T00:00:00+00:00",
        "resets_at": "2026-09-01T00:00:00+00:00"
    },
    "rate_limit": {
        "requests_per_minute": 40,
        "cached_requests_per_minute": 300
    }
}

plan and period are null for an account that has neither yet, so read them before you use them. The OpenAPI document describes the response in full.

Going over a limit

A request that goes over either limit is answered with HTTP status 429 and a Retry-After header. Nothing is rendered and no credit is spent, so waiting the stated number of seconds and sending the request again is all it takes. The errors page lists the other statuses worth retrying.

If you are working through a queue of pages, spread the requests out instead of firing them all at once. A short pause between calls keeps you inside the limit and finishes the batch sooner than a burst that spends most of its time being refused. Pace that against the render number: a batch made mostly of captures you already hold is answered from the cache and may run far faster than your render limit alone suggests.

Or hand the pacing to us. Every item of an asynchronous batch is counted against the same render budget, so a batch buys no extra capacity, but an item that meets the limit waits and asks again instead of being refused. Submitting one spends neither budget and runs under a small throttle of its own of 10 submissions per minute.

The playground

The playground has a limit of its own: one capture per minute, whatever your account may do through the API. It is there to try options out by hand, so the pace is set for a person rather than for a script.