Changelog
Every change to the API that a caller can notice, newest first. Changes to this site, to billing or to anything behind the endpoint are not here; if your code cannot tell it happened, it is not a change to the API.
https://api.screenshotbuddy.io/v1. Everything else has been an addition, and everything under /v1
still is, which is why the version in the path has not moved. What the move did not touch is exactly what
the promise below covers: no code was renamed, no parameter repurposed and no signature invalidated.
What we promise
The error codes and the status each one answers with are a
published contract. Renaming a code, or changing which situation it covers, is a breaking change: an
integration that retries on render_timeout and gives up on
blocked_host would silently start doing the wrong thing. That
waits for a version bump in the path.
Adding a code is not a breaking change, so treat one you do not recognise as the status it arrived with. The
human message next to it is free to be reworded, made more specific or translated at any time,
which is why nothing should ever branch on it.
New request parameters and new response headers arrive the same way: added, never repurposed. The OpenAPI document is generated from the same definitions the API validates against, so it is the machine-readable version of this page's present tense.
5 August 2026
The API moved to a host of its own
The API is served from https://api.screenshotbuddy.io/v1. Only the base URL changed: everything after
/v1 is the same path it was, and the parameters, the headers, the tokens, the signing scheme and
every signed URL you have already minted all carry over untouched. A signed URL keeps verifying because the
signature covers the canonical query and nothing else — the host was never part of what was signed, so a
URL built against the old base URL is valid against the new one without being reissued.
Change the base URL your client is built on and you are done. The OpenAPI document, the Postman collection and both reference clients already name the new one, so re-importing or re-copying is enough.
The old /api/v1/* paths are not redirected, because a redirect would be a quiet way of keeping two
homes alive and would send your Authorization header to a host you did not choose. They answer
410 with the code api_moved and a message naming
the new base URL instead, so an integration nobody updated diagnoses itself in a single response rather than in
a support thread.
Every error code.
Renders can be queued and collected later
POST https://api.screenshotbuddy.io/v1/renders takes up to
20 captures in one JSON body,
queues them and answers 202 straight away, so a slow page is no longer a connection your own stack
has to hold open and a list of pages is no longer a throttled loop you have to write. Each item takes the
parameters a render request takes.
GET https://api.screenshotbuddy.io/v1/renders/{id} reports where the batch has got to and hands back a
signed URL per finished item, and a submission that
carried a webhookUrl is posted the same document once, signed with the submitting token's signing
secret. Every item runs the lifecycle a synchronous call runs, permissions, credits and cache included, paced
against the account's own render limit; an item that meets it waits rather than failing. Nothing changes for
callers who do not use it: GET https://api.screenshotbuddy.io/v1/snap is untouched.
How batch and async renders work.
Cache hits stopped spending the render budget
The per-minute rate limit split in two. A fresh render spends your plan's render budget, exactly as before. An
answer served from your cache now comes out of a separate budget of
300 per minute, flat across plans, and a 304 answered out of
your cache counts against neither, so a page of twenty embedded signed-URL thumbnails no longer
costs a minute's render allowance. A request refused for validation or for a permission the token was never
granted stopped using an attempt at all.
X-RateLimit-Limit and X-RateLimit-Remaining now describe the budget the answer was
counted against, with X-Cache as the tell; a 304 answered out of your cache carries
neither, and a 429 names the limit that refused next to its Retry-After.
GET https://api.screenshotbuddy.io/v1/usage gained rate_limit.cached_requests_per_minute beside the
render limit it already reported, and on the MCP surface check-usage and the protocol calls
stopped spending render slots too.
How the two budgets work.
A fresh render can answer 304 as well
If-None-Match used to be answered by the cache alone. A request that had to render, because it
sent cache=0 or because the entry had lapsed, sent the whole file back even when the bytes it had
just rendered carried the tag the caller said they held. That request is now answered 304 with no
body, alongside its ETag, the X-Cache it would have carried and the
X-RateLimit pair.
This one is not the free 304 of the entry above. The page was loaded before we could say it looks
the same, so it costs the credit and the render slot the 200 it replaced would have cost, and it
is in your usage history at one credit like any other render. What you save is the download. Callers who never
send If-None-Match see nothing change.
Together with cache=0 it is a change monitor: send the tag of your last capture on whatever
schedule the page deserves, and a 200 means the page changed while a 304 means it
did not.
How caching works.
Answers say what the target page answered
A screenshot of a login wall, of a 404 page or of a rate limit notice came back as a clean
200 with a real image in it, and nothing in the answer said it was not the page you meant.
Screenshot answers now carry X-Target-Status, the status the captured page itself answered while
it was loading, on the 200 and on both kinds of 304. A rendered 404 page
is a 200 from us carrying X-Target-Status: 404.
Nothing about billing moved: a 4xx or 5xx target costs the credit it always did,
because the page the server actually served was loaded and rendered. The header is absent when we were not told
a status, and absent means not known rather than 200: https://api.screenshotbuddy.io/v1/snap?pdf=1 never
carries it, and neither does a capture cached before this shipped. Asynchronous items report the same thing as
a new target_status field, and the take-screenshot MCP tool says so in words when the
target answered something other than a 2xx.
What the target answered.
4 August 2026
The API is served over MCP as well
https://screenshotbuddy.io/mcp speaks the Model Context Protocol over streamable HTTP, so a client can call the
API as tools instead of being told how to write the request. Three of them: take-screenshot hands
the image back in the conversation, create-pdf answers with a signed link to the rendering because
the file is too large to travel in a tool result, and check-usage serves the document
GET https://api.screenshotbuddy.io/v1/usage serves.
It takes the same bearer tokens in the same Authorization header, so there is no second credential
to mint and no OAuth step, and a token narrowed to one mode stays narrowed. The two rendering tools accept the
parameters their endpoints accept, checked by the same rules, and a render costs the credit it costs over HTTP
and is counted against the same per-minute budget. Nothing changes for callers who do not use it.
How the MCP server works.
3 August 2026
The API went from one endpoint with one way in to the surface described by the rest of this documentation. Everything below landed on the same day and none of it changes an answer a caller was already getting.
A Postman collection
GET https://api.screenshotbuddy.io/v1/postman.json answers with the API as a Postman collection, built from the same
definitions the OpenAPI document is built from. It carries every
endpoint with every parameter it accepts, takes its token from one collection variable, and arrives with the
optional parameters listed but switched off. It needs no token itself, so importing it is something you can do
before you have one. Importing the collection.
Signed URLs for direct embedding
GET https://api.screenshotbuddy.io/v1/snap/signed takes the same capture parameters as /snap plus a
tokenId, an optional expires and an HMAC-SHA256 signature over them, so
a capture can go straight into an <img> tag without a token travelling to the browser. Every
token gained a signing secret of its own, revealed on demand under
Settings » API tokens, and a signed request is metered,
billed, cached and rate limited exactly as the bearer request it stands in for.
How to sign a URL.
Response caching and request dedup
Asking for the same capture twice now answers from the rendering already made for your account, and a hit costs
no credit. Responses say which it was with X-Cache: HIT or MISS and carry an
ETag that If-None-Match turns into a 304; cache=0 opts out
and cacheTtl chooses the lifetime. Identical requests that arrive at the same moment are rendered
once and answered twice. How caching works.
Credit and request metadata headers
Every API response now carries X-Request-Id, and every authenticated one adds
X-Credits-Limit, X-Credits-Remaining and X-Credits-Reset, so running low
is something you read off a successful response rather than discover on a failed one. The request id is the
same value as the request_id in an error
envelope and in our logs, which makes it the thing to quote in a support request.
What the headers mean.
The capture options the renderer already supported
/snap accepted a URL and little else. It now takes format, quality,
width, height, scale, delay, selector, the
four clip fields, omitBackground, waitForSelector and
waitUntil for screenshots, and paperFormat, landscape and the four
margins for PDFs. Options that contradict each other are refused rather than quietly ignored: naming a
selector, a clip and fullPage together is three different answers to
where the capture starts. Screenshot options and
PDF options.
Token permissions, expiry and rotation
A token now carries the two permissions the product actually has, screenshot and pdf,
checked before any credit is spent: asking for a PDF with a screenshot only token answers 403 and
missing_ability. Tokens can also be given an expiry when you
create them and rotated in place, which mints a replacement and deletes the old value in one step. Tokens that
existed before this were given both permissions, so none of them changed behaviour.
Permissions, expiry and rotation.
A usage endpoint and machine-readable error codes
GET https://api.screenshotbuddy.io/v1/usage reports your remaining, used and granted credits, your plan, when the
period resets and the rate limit in effect. It answers 200 whatever state the account is in and
has a throttle of its own, so polling it never eats into the budget for renders.
The usage endpoint.
Alongside it, every non-2xx answer under api/* became one envelope: a stable code, a
human message, an optional errors map and a request_id. Two failures
that used to share a status and differ only in their English now differ in their code, so a client can tell a
blocked host from a malformed URL without reading a sentence.
Every error code.
See the current surface, not the history
The playground runs the API as it stands today, with every option this page introduced, and hands you the matching code.