Generate the first thumbnail.
Use a fixed 16:9 viewport for consistent directory, bookmark, or link-preview artwork. The response body is the JPEG itself, so it can be written directly to a file or object store.
curl --fail-with-body https://latchshot.fly.dev/v1/render \
-H "Authorization: Bearer $LATCHSHOT_API_KEY" \
-H 'Content-Type: application/json' \
--data '{
"url": "https://example.com",
"format": "jpeg",
"width": 800,
"height": 450,
"delay": 250,
"reducedMotion": true
}' \
--output example-thumbnail.jpg
A successful response includes Content-Type: image/jpeg. Keep fullPage off: a thumbnail should preserve a predictable viewport crop rather than compress an arbitrarily tall document.
The email-only Free plan includes 100 successful renders each UTC calendar month. Failed direct renders do not consume quota, and automatic overages are disabled.
Separate capture dimensions from display dimensions.
Latchshot's width and height select the browser viewport. They do not independently downscale a desktop page into a smaller output. If the source must use its desktop breakpoint, capture at 1200×675 or 1440×810, store that master, then derive smaller files with your image CDN or media pipeline.
| Need | Capture choice | Reason |
|---|---|---|
| Compact mobile preview | 400×300 | Lets the page render at its narrow responsive breakpoint. |
| Directory or bookmark card | 800×450 | Produces a direct 16:9 artifact with moderate byte size. |
| Desktop-layout master | 1200×675 | Preserves a desktop breakpoint before downstream resizing. |
| High-density master | 1200×675 at scale 2 | Returns 2400×1350 pixels; use only when the delivery surface needs it. |
Store by normalized URL, not by request time.
Do not render inside the page-view path. Queue the capture when a URL is added or becomes stale, write the result once, and let the application serve the stored object. A stable hash avoids leaking the target URL into filenames.
import { createHash } from 'node:crypto';
import { mkdir, writeFile } from 'node:fs/promises';
const target = new URL('https://example.com');
target.hash = '';
const response = await fetch('https://latchshot.fly.dev/v1/render', {
method: 'POST',
headers: {
authorization: `Bearer ${process.env.LATCHSHOT_API_KEY}`,
'content-type': 'application/json',
},
body: JSON.stringify({
url: target.href,
format: 'jpeg',
width: 1200,
height: 675,
reducedMotion: true,
}),
});
if (!response.ok) throw new Error(await response.text());
const key = createHash('sha256').update(target.href).digest('hex');
await mkdir('thumbnails', { recursive: true });
await writeFile(`thumbnails/${key}.jpg`,
Buffer.from(await response.arrayBuffer()));
For object storage, keep the same key and replace the final file write with the provider's upload call. Record the capture timestamp and response diagnostics beside the object so refresh and support decisions do not depend on guesswork.
Rasterize hosted HTML after deployment.
An artifact grid should not mount one live iframe per card. Queue one screenshot when a public HTML artifact becomes available, store the PNG beside the artifact, and let the grid request an ordinary image. Keep the interactive HTML for the detail view.
curl --fail-with-body https://latchshot.fly.dev/v1/render \
-H "Authorization: Bearer $LATCHSHOT_API_KEY" \
-H 'Content-Type: application/json' \
--data '{
"url": "https://artifacts.example.com/build-8f31/",
"format": "png",
"width": 1280,
"height": 800,
"waitUntil": "networkidle",
"timeout": 30000,
"reducedMotion": true
}' \
--output preview.png
networkidle is useful when a finite set of fonts, scripts, and assets must finish before capture. It is not a universal “page ready” signal: analytics, polling, or long-lived connections can consume the full timeout. For those pages, prefer domcontentloaded with a bounded delay. If correctness depends on an application-specific ready flag, authenticated state, or an interaction, keep that capture in Playwright you control.
- Artifact readyQueue a bounded background job with the new public artifact URL and version.
- CaptureRequest one fixed-size PNG using the weakest readiness signal that is reliable for the page.
- CommitWrite the bytes under a versioned object key, then update the artifact row only after storage succeeds.
- FailureKeep the previous image or a deterministic fallback, leave new work retryable, and never block deployment.
- Grid viewRender the stored image; reserve the live iframe for the focused interactive view.
Use a bounded batch rather than unbounded fan-out. A nullable preview URL makes deployment order safe: old rows and in-progress renders keep their fallback, while completed rows move to the stored image without changing the underlying artifact.
Refresh on product events, not every visit.
The right schedule depends on how quickly a preview becomes misleading. A directory homepage might refresh monthly; a customer-edited landing page may refresh when its URL or publication version changes.
- New URLQueue the first capture and show a text or brand-color fallback until it completes.
- Content eventRegenerate after an explicit publish or URL-change signal when your product owns one.
- Age thresholdRefresh stale objects in a bounded background batch, such as every 7–30 days.
- Page viewServe the cached object only. Never make user latency depend on a live browser job.
Use one bounded retry for 429, 502, or 504 responses and retain the prior good thumbnail. Treat an invalid target as a product-data problem rather than an infinite retry queue.
Make the screenshot a fallback, not the identity.
A website crop can add context, but it may contain consent banners, transient promotions, or text that becomes illegible at small sizes. Use an explicit logo or owner-supplied image when one exists; use the stored screenshot when it does not; keep a deterministic text or color placeholder for failures.
- Owner-provided image or logo: highest control and clearest identity.
- Stored website thumbnail: useful visual context from the public page.
- Deterministic placeholder: stable initials, hostname, or category treatment while capture is unavailable.
If you ingest remote Open Graph images, validate that fetch path independently. Latchshot's network protections apply to its render request; they do not secure a separate image fetch performed by your application.
Keep the capture job public and bounded.
- Only public HTTP/HTTPS pages on ports 80 and 443 are supported.
- Private, loopback, link-local, special-use, credential-bearing, and mixed public/private DNS targets are rejected.
- Cookies, authenticated sessions, arbitrary page actions, CAPTCHA solving, proxies, and anti-bot bypass are not available.
- Your product remains responsible for permission to capture, store, refresh, and display the target page.
Keep Playwright in your own environment when the preview needs a login, private host, click sequence, or application-specific state. The API versus Playwright guide maps that boundary directly.
Match the plan to refresh volume.
Free includes 100 successful renders per UTC calendar month. Launch is $19 for 2,500, Build is $49 for 12,000, and Scale is $149 for 50,000. Automatic overages are disabled.
A catalog of 2,000 URLs refreshed monthly fits within Launch with room for new items and controlled retries. A 10,000-URL monthly refresh fits within Build. These are quota examples, not throughput or reliability promises.
The current supported-page benchmark rendered 197/200 pages, with 2.42-second p50 and 7.68-second p95 end to end. It is engineering evidence, not an SLA; review the method and boundaries before a production cutover.
Build one stored thumbnail first.
Start with a representative public page, verify its crop and byte size, then choose the refresh policy before issuing captures in bulk.