# 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.

> One entry on this page is a breaking change, and it is the newest one: the API moved to a host of its own, `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](https://screenshotbuddy.io/documentation/errors.md) 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](https://api.screenshotbuddy.io/v1/openapi.json) 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](https://api.screenshotbuddy.io/v1/openapi.json), 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. See [every error code](https://screenshotbuddy.io/documentation/errors.md).

### 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](https://screenshotbuddy.io/documentation/signed-urls.md) 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. See [how batch and async renders work](https://screenshotbuddy.io/documentation/batch-renders.md).

### 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. See [how the two budgets work](https://screenshotbuddy.io/documentation/rate-limits.md).

### 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. See [how caching works](https://screenshotbuddy.io/documentation/caching.md).

### 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`. See [what the target answered](https://screenshotbuddy.io/documentation/taking-screenshots.md).

## 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. See [how the MCP server works](https://screenshotbuddy.io/documentation/mcp-server.md).

## 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](https://api.screenshotbuddy.io/v1/openapi.json) 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. See [importing the collection](https://screenshotbuddy.io/documentation/sdks.md).

### 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](https://screenshotbuddy.io/settings/api-tokens), and a signed request is metered, billed, cached and rate limited exactly as the bearer request it stands in for. See [how to sign a URL](https://screenshotbuddy.io/documentation/signed-urls.md).

### 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. See [how caching works](https://screenshotbuddy.io/documentation/caching.md).

### 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](https://screenshotbuddy.io/documentation/errors.md) and in our logs, which makes it the thing to quote in a support request. See [what the headers mean](https://screenshotbuddy.io/documentation/rate-limits.md).

### 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. See [screenshot options](https://screenshotbuddy.io/documentation/taking-screenshots.md) and [PDF options](https://screenshotbuddy.io/documentation/creating-pdfs.md).

### 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. See [permissions, expiry and rotation](https://screenshotbuddy.io/documentation/authentication.md).

### 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. See [the usage endpoint](https://screenshotbuddy.io/documentation/rate-limits.md).

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. See [every error code](https://screenshotbuddy.io/documentation/errors.md).
