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 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.
<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 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, 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.
$thumbnail = signedSnapUrl(
[
'url' => $listing->website,
'width' => '1200',
'height' => '750',
'scale' => '2',
'format' => 'webp',
],
42,
getenv('SCREENSHOTBUDDY_SIGNING_SECRET'),
);
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.
-
widthandheight - 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=2captures 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. -
formatandquality -
png,jpegorwebp, defaulting topng. A grid of thumbnails is the case a lossy format was made for, andquality, from 1 to 100, is how you trade file size against detail. It applies to the lossy formats only, so sending it alongsidepngis rejected rather than ignored.
Settle all four in the 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.
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"}]}'
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 page has the rest.
Start with one thumbnail
Get a capture looking right in the playground, read how the URL is signed in the signed URLs documentation, 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 differ in how many renders you get each month and how many you can ask for per minute.