---
name: screenshotbuddy-api
description: Take a screenshot of a URL, capture a website thumbnail, or render a page to PDF through the ScreenshotBuddy HTTP API. Use when writing code that turns a web page into a PNG, JPEG or WebP image or into a PDF file, when integrating a screenshot API or page rendering service, when embedding a rendered page in an app, a dashboard or an email, or when the user mentions ScreenshotBuddy. Covers authenticating with a bearer token, the screenshot and PDF modes of the snap endpoint, the JSON error contract and which failures are worth retrying, checking credits before spending them, and signed URLs for embedding.
---

# ScreenshotBuddy API

ScreenshotBuddy renders any public web page as an image or a PDF over a single authenticated `GET` request. A successful call answers with the file itself, so treat the body as binary and write it to a file rather than parsing it as JSON. Every refusal is one JSON envelope.

Base URL: `https://api.screenshotbuddy.io/v1`

## Prefer the MCP server when the client can use one

The same API is served over the Model Context Protocol at `https://screenshotbuddy.io/mcp`, authenticated by the same bearer tokens. If you are working in a client that can consume an MCP server, connect it instead of hand-writing HTTP calls: the tool definitions carry the parameter list and its validation with them.

```bash
claude mcp add --transport http screenshotbuddy https://screenshotbuddy.io/mcp --header "Authorization: Bearer <key>"
```

The tools, what each answers with and what each costs are documented at [The MCP server](https://screenshotbuddy.io/documentation/mcp-server.md).

This skill is for when you are writing integration code: a backend that calls the API, a script, a client library, a job that renders pages on a schedule.

## Getting a key

The API takes a personal access token in an `Authorization` header:

```
Authorization: Bearer <key>
```

There is deliberately no endpoint that mints one, so do not go looking for a sign-up call and do not try to create an account. Ask the person you are working with to create a token under [Settings, API tokens](https://screenshotbuddy.io/settings/api-tokens) and to hand you the value, which is shown once. In the code you write, read it from an environment variable or a secrets manager. Never commit it, and never put it anywhere a browser receives.

A token can be narrowed to one mode, screenshots or PDFs. A request answered `missing_ability` is not the wrong token, it is a token deliberately narrowed away from what you asked for.

## Check where the account stands before spending a credit

```bash
curl "https://api.screenshotbuddy.io/v1/usage" -H "Authorization: Bearer $KEY"
```

It answers `200` with `credits`, `plan`, `period` and `rate_limit`. It costs no credit and runs under a throttle of its own, so polling it never eats into the render budget. It reports state rather than refusing on it: an account with nothing left is described with a `200` and the numbers that say so, where a render answers `402`.

Every render costs one credit, and a failed render is refunded. An identical repeat of a request is answered from the rendering already made, for no credit, marked `X-Cache: HIT`. Rendered answers carry an `ETag` you can send back as `If-None-Match` to get a `304` with no body. A `304` the cache can answer costs neither a credit, nor the download, nor anything against a rate limit; a `304` that had to render first, because you sent `cache=0` or the entry had lapsed, costs the render and saves you only the download. That second case is how you watch a page for changes: `cache=0` with the tag of your last capture, where `200` means it changed and `304` means it did not.

There are two rate limits, and `rate_limit` reports both. `requests_per_minute` is what the plan allows in fresh renders, counted only when a request actually has to render, a `cache=0` request included. `cached_requests_per_minute` is a flat and far larger budget that answers served out of the cache come out of instead, so a batch of repeats does not eat the renders. A request refused for its validation or its permissions counts against neither; being refused for no plan or no credits counts as a render, because it asked for one. `X-RateLimit-Limit` and `X-RateLimit-Remaining` describe whichever budget paid for the answer you are holding, and a `429` names the one that refused you.

## Taking a screenshot

```bash
curl "https://api.screenshotbuddy.io/v1/snap?url=https%3A%2F%2Fexample.com&fullPage=true" \
  -H "Authorization: Bearer $KEY" \
  --output screenshot.png
```

`url` is the only required parameter. It has to be a publicly reachable `http://` or `https://` address; private, loopback and internal hosts are refused.

A screenshot answer carries `X-Target-Status`, the status the page itself answered while it was loading. A login wall and a `404` page render just as crisply as the page you wanted, so check it before you store a capture. It is absent when we were not told one, which means not known rather than `200`, and PDF answers never carry it. A `4xx` or `5xx` target still costs a credit: the page the server served was rendered.

## Two modes that do not overlap

`pdf` set to `true` switches the same endpoint into PDF mode:

```bash
curl "https://api.screenshotbuddy.io/v1/snap?url=https%3A%2F%2Fexample.com&pdf=true&landscape=true" \
  -H "Authorization: Bearer $KEY" \
  --output page.pdf
```

Most parameters belong to one mode. `fullPage`, `format` and `quality` are screenshots only; `landscape`, `paperFormat` and the margin options are PDFs only. A parameter sent to the mode that does not accept it is rejected with a `422`, not ignored, so you cannot build one parameter set and flip `pdf` on and off over it. Build the query per mode.

The full parameter lists, with types, defaults and ranges, are in [taking screenshots](https://screenshotbuddy.io/documentation/taking-screenshots.md) and [creating PDFs](https://screenshotbuddy.io/documentation/creating-pdfs.md). Look a parameter up there rather than guessing its name.

## Rendering a slow page, or a whole set of them

Do not build a throttled loop over `/snap`. `POST https://api.screenshotbuddy.io/v1/renders` takes a JSON body of up to 20 items, each one the same parameters a render request takes, plus an optional `webhookUrl`. It answers `202` immediately, and you collect the results by polling `/renders/{id}` or from the one signed webhook sent when the batch is done.

Every item runs the same billed lifecycle as a synchronous call, so nothing about permissions, credits or caching changes. Items are paced against the account's render limit by us: an item that meets it waits and retries rather than failing. Results arrive as signed URLs, which is why an item may not send `cache=0` and why its `cacheTtl` has a floor.

The submission body, the poll document, the webhook signature and the retention window are on the [batch and async renders](https://screenshotbuddy.io/documentation/batch-renders.md) page.

## Branch on the error code

Every non-2xx answer is JSON in one envelope:

```json
{
  "code": "credit_limit_reached",
  "message": "Every credit in the current period is spent.",
  "request_id": "1f3c0e6a-6a5d-4a4c-9a3f-0a3f7d1c2b84"
}
```

An `errors` object joins it when parameters are at fault, naming each one. Branch on `code`, never on `message`, which is free to be reworded, and never on the status alone, which several codes share.

Worth retrying with backoff, and the credit is already refunded:

- `render_timeout`, `upstream_error`, `render_failed`

Worth retrying, but back off further, and do not read it as a bug in your request:

- `rate_limited`, which carries `Retry-After`; wait for it
- `upstream_rate_limited`, where the rendering service was busy; it carries `Retry-After` too, and the wait is short
- `upstream_quota_exceeded`, where the rendering service itself is out of capacity for the period

Not worth retrying, the URL is the problem:

- `invalid_url`, the string is not a URL the API accepts
- `blocked_host`, it points somewhere private or internal, and reformatting will not help
- `target_unreachable`, the page itself could not be reached or loaded

Not worth retrying, fix the request or ask a person:

- `validation_failed`, read the `errors` object and correct the query
- `unauthenticated`, the key is missing, unknown or revoked
- `missing_ability`, the token is narrowed away from this mode
- `email_unverified`, the account has not confirmed its email address yet
- `no_active_plan`, `credit_limit_reached`, both billing, both wanting a person rather than a retry
- `server_error`, worth reporting with the `request_id` from the envelope

Log the `request_id` on every failure. It is what support can trace a request by.

Every code, with the status it answers with, is on the [errors](https://screenshotbuddy.io/documentation/errors.md) page.

## Embedding a render in a browser

When the thing fetching the image is an `<img src>`, a CDN or an email client, do not hand it a bearer token. Use `/snap/signed`, which carries an HMAC of the query instead, built on your own server with the token's signing secret so the secret never travels. The algorithm, with a worked example and reference implementations, is on the [signed URLs](https://screenshotbuddy.io/documentation/signed-urls.md) page.

## Looking things up

- [Documentation index](https://screenshotbuddy.io/llms.txt), every page with a line saying what is on it
- [OpenAPI document](https://api.screenshotbuddy.io/v1/openapi.json), the whole API as OpenAPI 3.1, no token needed, and the thing to generate a client from
