# Website thumbnail API

A directory, a marketplace, a dashboard of client sites and a link preview all want the same thing: a picture of somebody else's page, sitting next to a link to it. ScreenshotBuddy renders that picture on request and answers with the image itself, so the thumbnail beside a listing becomes a URL you build rather than a file you own. There are no browsers to run on your side, no bucket to fill and nothing to refresh.

## The integration is an img tag

An `<img>` tag cannot send an `Authorization` header, which is why showing a capture usually means proxying it through a service of your own whose only job is to add one. A [signed URL](https://screenshotbuddy.io/documentation/signed-urls.md) removes that service. You build the capture request where you already render the page, sign it with a secret that never leaves your server, and put the result in the `src`.

```html
<img src="https://api.screenshotbuddy.io/v1/snap/signed?url=https%3A%2F%2Fexample.com&width=1200&height=750&scale=2&format=webp&tokenId=42&signature=141ed417a4d0d651f42c318860a6985db0452fd57a2886c4b40bf3983e21ef3d"
     alt="A screenshot of example.com"
     loading="lazy">
```

The secret behind that example is not a real one, but the signature is: it comes from the same code that verifies your requests.

The URL is the whole request. It carries the page to capture, the viewport it is captured at, the id of the token it belongs to, and an HMAC-SHA256 signature over all of that, keyed with the token's signing secret. Every parameter is covered, so whoever sees the URL can fetch exactly the capture it describes and nothing else: they cannot point it at another page, widen the viewport, or make any other request against your account.

Building one is a function call in the template that renders the listing. `signedSnapUrl` below is the reference implementation from the [signed URLs documentation](https://screenshotbuddy.io/documentation/signed-urls.md), which spells out the canonical string it signs. The token id is the number in front of the pipe in the token you copied; the secret belongs beside it in your environment.

```php
$thumbnail = signedSnapUrl(
    [
        'url' => $listing->website,
        'width' => '1200',
        'height' => '750',
        'scale' => '2',
        'format' => 'webp',
    ],
    42,
    getenv('SCREENSHOTBUDDY_SIGNING_SECRET'),
);
```

No expiry, on purpose. A signed URL without one keeps working for as long as the token behind it does, which is what an embed wants.

## Nothing to store, nothing to invalidate

The usual way to put a thumbnail on a listing page is a pipeline: a job that captures the site when the listing is created, a bucket to keep the file in, a path on your CDN, a scheduled task to recapture the sites that have been redesigned since, and a piece of code to invalidate the old copy when it does. Every one of those is somewhere a thumbnail can quietly go missing, and all of it exists to serve an image that was never yours.

A signed URL has none of it. The first person to load the listing page causes the capture. Every fetch after that is answered from the rendering already made for you, which costs no credit and says so with `X-Cache: HIT`. Entries belong to your own account, so nobody else's request is ever answered with a rendering you paid for.

Freshness stops being a job you run and becomes a number you set. A rendering is kept for 86400 seconds unless you send `cacheTtl`, which takes anything from `60` to `2592000` seconds, the second being thirty days. Once that time is up the next request renders the page again. Pick it from how often the sites you list actually change rather than from how often people look at them.

Leaving `expires` off is what makes the caches in front of us worth having. The signature is a function of the parameters alone, so the same capture signed with the same secret produces the same signature every time, and the URL your page renders today is character for character the URL it rendered yesterday. A browser or a CDN that already holds that image recognises it and does not come back to us at all. Add an expiry and the URL changes every time you build it, which is a fresh entry in every cache, so keep that for the links that ought to stop working rather than for the thumbnails you want cached.

## Sharp on a retina screen

Four parameters shape a thumbnail, and they are the same ones on [any screenshot](https://screenshotbuddy.io/documentation/taking-screenshots.md).

- `width` and `height`: the viewport the page is loaded at, in pixels, anywhere from 1 to 10000. They have to be sent together. This is the shape of the browser window the site thinks it is being viewed in, so it is also what decides whether you get the desktop layout or the mobile one.
- `scale`: the device scale factor the browser renders at, a whole number from 1 to 3 for screenshots, defaulting to 1. Sending `scale=2` captures the page the way a retina display draws it, which is what keeps a thumbnail from looking soft on the screens most people are reading your listings on.
- `format` and `quality`: `png`, `jpeg` or `webp`, defaulting to `png`. A grid of thumbnails is the case a lossy format was made for, and `quality`, from 1 to 100, is how you trade file size against detail. It applies to the lossy formats only, so sending it alongside `png` is rejected rather than ignored.

Settle all four in the [playground](https://screenshotbuddy.io/playground) before you sign anything. Changing your mind about a parameter means signing again, and the playground will hand you the signed URL once the result looks right.

## What a page of thumbnails costs

One credit per fresh render, and nothing at all for a repeat. An identical request is answered from the rendering we already hold, which arrives without waiting for a browser and is not charged for, because there was nothing to render.

The rate limit splits along the same line, and that is the part that matters to a listing page. A cached answer is counted against a budget of its own, 300 per minute flat and the same for every account, rather than against the renders your plan allows per minute. A directory page that fetches twenty thumbnails the moment it loads is twenty hits, paced by that far larger number, and it leaves your render allowance for the captures that have still to be made.

Because a hit costs us nothing, it is served even when your credits have run out or your plan has lapsed. A request that has to render is refused in that situation; one we can answer from a rendering you already paid for is not, so a page somebody has seen before keeps its pictures.

One thing to know before you build a retry around this. A signed answer never carries the `X-RateLimit-` or `X-Credits-` headers, because whoever fetched the image is not who your plan size and your spending are for. A fetch that goes over a limit is still refused with a `429` and a `Retry-After`, so pace a page of embeds by the limits themselves rather than by what an answer tells you.

## Prewarming a large first import

The one moment the pattern above asks something of a visitor is a listing nobody has looked at yet: that first fetch waits for a browser to load somebody else's site. When you import a few thousand listings at once, get the captures made before anyone arrives.

`POST /renders` takes up to 20 captures in one submission, each item accepting the parameters a single screenshot request accepts, and answers immediately with a batch document. You collect the results by polling that document or by giving it a `webhookUrl` to post to once every item has finished.

```bash
curl "https://api.screenshotbuddy.io/v1/renders" \
  -H "Authorization: Bearer <your API key>" \
  -H "Content-Type: application/json" \
  -d '{"items":[{"url":"https://example.com","width":1200,"height":750,"scale":2,"format":"webp"},{"url":"https://example.org","width":1200,"height":750,"scale":2,"format":"webp"}]}'
```

A batch is submitted with a bearer token rather than a signature, so this call belongs in your import script and not in a template.

Here is why it pays off in this particular case. A rendering is kept under the exact request that produced it, and a signed URL and a bearer request for the same capture on the same account share one entry. So a batch item and the `src` on your listing page are the same picture as far as the cache is concerned, and the first person to open the page finds hits waiting rather than renders. The parameters have to match exactly, though: a different viewport, scale or format describes a different picture and is therefore a different entry.

A batch buys no extra capacity. Every item is counted against the same renders per minute your plan allows, and an account that submits twenty renders them at the pace it would have one by one. What changes is who does the waiting: an item that meets your per-minute limit goes back to the queue and asks again instead of failing, which is the whole reason to send a set rather than write a loop.

Three rules exist here and nowhere else. An item may not turn caching off, because the cache is how an asynchronous result is handed over at all. Its `cacheTtl` has a floor of 3600 seconds, so a rendering cannot lapse while its own batch is still draining. And two items may not describe the same capture, which on an import list is worth a deduplicating pass before you submit. Across all of your batches at once, at most 100 items may be outstanding, so a very large import goes in waves rather than in one go. The [batch and async renders](https://screenshotbuddy.io/documentation/batch-renders.md) page has the rest.

## Start with one thumbnail

Get a capture looking right in the [playground](https://screenshotbuddy.io/playground), read how the URL is signed in the [signed URLs documentation](https://screenshotbuddy.io/documentation/signed-urls.md), and paste the result into a template. New accounts get 200 renders a month for free, which is enough to put thumbnails on a real listing page before anything is paid for. The [plans](https://screenshotbuddy.io/pricing) differ in how many renders you get each month and how many you can ask for per minute.

[Create an account](https://screenshotbuddy.io/register) to get a token and its signing secret.
