Screenshots are not just for documentation or screen recording. In modern web development, screenshot APIs have become infrastructure — quietly powering everything from social sharing previews to automated visual regression pipelines. If you are building a SaaS, running a web agency, or maintaining a large web application, there is a good chance you can benefit from programmatic screenshot generation.
In this post, we cover five practical ways to integrate a screenshot API into your application, with real code examples using len.sh. By the end, you will have concrete patterns you can adapt for your own projects.
The five use cases:
- Dynamic OG image generation
- Website monitoring and change detection
- PDF-style document generation
- Rich link previews
- Visual regression testing
1. Dynamic OG Image Generation
When someone shares a link on Twitter, LinkedIn, or Slack, the platform fetches the og:image metadata and renders a preview card. A compelling, dynamic image can dramatically increase click-through rates. Static images are limiting — you want images that reflect the actual content of each page.
The recommended approach is the dedicated /v1/og endpoint. It generates branded 1200x630 Open Graph images directly from structured parameters — no HTML template to build or host. You pass in a title, subtitle, badge, URL, brand name, brand color, and theme, and it returns a ready-to-use PNG.
Example: Generating a branded OG image with /v1/og
curl "https://api.len.sh/v1/og?title=Hello+World&subtitle=A+great+article&badge=Blog&url=yoursite.com&brand_name=YourBrand&brand_color=%234F46E5&theme=light&api_key=YOUR_API_KEY" \
--output og-image.png
In a Node.js server (e.g. Next.js API route or Express endpoint), you can generate this on demand:
// pages/api/og.js (Next.js)
export default async function handler(req, res) {
const { title, subtitle } = req.query;
const ogUrl = new URL("https://api.len.sh/v1/og");
ogUrl.searchParams.set("title", title);
ogUrl.searchParams.set("subtitle", subtitle || "");
ogUrl.searchParams.set("badge", "Blog");
ogUrl.searchParams.set("url", "yoursite.com");
ogUrl.searchParams.set("brand_name", "YourBrand");
ogUrl.searchParams.set("brand_color", "#4F46E5");
ogUrl.searchParams.set("theme", "light");
ogUrl.searchParams.set("api_key", process.env.LENSH_API_KEY);
const response = await fetch(ogUrl.toString());
const buffer = await response.arrayBuffer();
res.setHeader("Content-Type", "image/png");
res.setHeader("Cache-Control", "public, max-age=86400");
res.send(Buffer.from(buffer));
}
Then in your page’s <head>:
<meta property="og:image" content="https://yoursite.com/api/og?title=Hello+World&subtitle=A+great+article" />
The /v1/og endpoint handles layout, typography, and branding for you. No HTML template to maintain and no viewport configuration to get right.
If you need full pixel-level control over the design, you can still use the /v1/screenshot endpoint with an HTML template URL or the html parameter to render arbitrary markup at 1200x630. The dedicated OG endpoint covers the vast majority of use cases with far less setup.
2. Website Monitoring and Change Detection
Visual monitoring is one of the most underrated applications of screenshot APIs. Traditional uptime monitors tell you when a site is down, but they cannot tell you when a page quietly breaks — a misaligned layout, a missing hero image, or defaced content.
By periodically capturing screenshots and comparing them, you can catch visual regressions before users do.
Example: Scheduled screenshot + image comparison
Using node-cron and pixelmatch for comparison:
import cron from "node-cron";
import fetch from "node-fetch";
import fs from "fs/promises";
import { PNG } from "pngjs";
import pixelmatch from "pixelmatch";
const API_KEY = process.env.LENSH_API_KEY;
const TARGET_URL = "https://yoursite.com";
const BASELINE_PATH = "./baseline.png";
const CURRENT_PATH = "./current.png";
const DIFF_THRESHOLD = 0.05; // 5% pixel difference threshold
async function captureScreenshot(outputPath) {
const url = `https://api.len.sh/v1/screenshot?url=${encodeURIComponent(TARGET_URL)}&width=1280&height=800&format=png&api_key=${API_KEY}`;
const res = await fetch(url);
const buffer = await res.arrayBuffer();
await fs.writeFile(outputPath, Buffer.from(buffer));
}
async function compareScreenshots() {
const baselineData = PNG.sync.read(await fs.readFile(BASELINE_PATH));
const currentData = PNG.sync.read(await fs.readFile(CURRENT_PATH));
const { width, height } = baselineData;
const diff = new PNG({ width, height });
const numDiffPixels = pixelmatch(
baselineData.data,
currentData.data,
diff.data,
width,
height,
{ threshold: 0.1 }
);
const diffRatio = numDiffPixels / (width * height);
return { diffRatio, numDiffPixels };
}
// Run every 15 minutes
cron.schedule("*/15 * * * *", async () => {
console.log("Running visual check...");
await captureScreenshot(CURRENT_PATH);
const { diffRatio } = await compareScreenshots();
if (diffRatio > DIFF_THRESHOLD) {
console.error(`Visual change detected! Diff ratio: ${(diffRatio * 100).toFixed(2)}%`);
// Send alert via Slack, PagerDuty, email, etc.
} else {
console.log(`All clear. Diff: ${(diffRatio * 100).toFixed(2)}%`);
// Update baseline
await fs.copyFile(CURRENT_PATH, BASELINE_PATH);
}
});
For a first run, capture your baseline:
curl "https://api.len.sh/v1/screenshot?url=https://yoursite.com&width=1280&height=800&format=png&api_key=YOUR_API_KEY" \
--output baseline.png
This is a lightweight alternative to full-blown visual monitoring services — and you own the comparison logic completely.
3. PDF-Style Document Generation
Traditional PDF generation in Node.js usually involves libraries like pdfkit, puppeteer, or wkhtmltopdf. These tools are powerful but come with trade-offs: complex APIs, layout quirks, or heavy dependencies.
If your report, invoice, or dashboard already renders correctly as a web page, screenshotting it to PDF is the most reliable approach. What you see in the browser is exactly what you get in the PDF.
Example: Generating a PDF invoice
curl "https://api.len.sh/v1/screenshot?url=https://yourapp.com/invoices/inv-1042&format=pdf&full_page=true&api_key=YOUR_API_KEY" \
--output invoice-1042.pdf
The full_page=true parameter captures the entire scrollable page, not just the viewport — essential for long documents.
In an Express endpoint that streams the PDF to the client:
import express from "express";
import fetch from "node-fetch";
const app = express();
app.get("/invoices/:id/pdf", async (req, res) => {
const invoiceUrl = `https://yourapp.com/invoices/${req.params.id}`;
const screenshotUrl = new URL("https://api.len.sh/v1/screenshot");
screenshotUrl.searchParams.set("url", invoiceUrl);
screenshotUrl.searchParams.set("format", "pdf");
screenshotUrl.searchParams.set("full_page", "true");
screenshotUrl.searchParams.set("width", "1200");
screenshotUrl.searchParams.set("api_key", process.env.LENSH_API_KEY);
const upstream = await fetch(screenshotUrl.toString());
res.setHeader("Content-Type", "application/pdf");
res.setHeader(
"Content-Disposition",
`attachment; filename="invoice-${req.params.id}.pdf"`
);
upstream.body.pipe(res);
});
You can use the delay parameter to wait for dynamic content to load before capturing. For example, if your charts render after a JavaScript animation:
curl "https://api.len.sh/v1/screenshot?url=https://yourapp.com/reports/q4&format=pdf&full_page=true&delay=2000&api_key=YOUR_API_KEY" \
--output q4-report.pdf
This waits 2000ms after page load before capturing, giving async data fetches and animations time to complete.
4. Rich Link Previews
Slack, Notion, and Twitter all show rich preview cards when you paste a URL. You can build the same experience in your own application — a chatroom, a bookmarking tool, a content aggregator, or any app where users share links.
The flow is: user pastes a URL, you call the screenshot API in the background, cache the result, and display the thumbnail alongside the link title and description.
Example: Express endpoint for link preview images
import express from "express";
import fetch from "node-fetch";
import NodeCache from "node-cache";
const app = express();
const cache = new NodeCache({ stdTTL: 3600 }); // 1-hour cache
app.get("/preview", async (req, res) => {
const { url } = req.query;
if (!url) return res.status(400).json({ error: "url is required" });
const cacheKey = `preview:${url}`;
const cached = cache.get(cacheKey);
if (cached) {
res.setHeader("Content-Type", "image/webp");
res.setHeader("X-Cache", "HIT");
return res.send(cached);
}
try {
const screenshotUrl = new URL("https://api.len.sh/v1/screenshot");
screenshotUrl.searchParams.set("url", url);
screenshotUrl.searchParams.set("width", "1280");
screenshotUrl.searchParams.set("height", "720");
screenshotUrl.searchParams.set("format", "webp");
screenshotUrl.searchParams.set("api_key", process.env.LENSH_API_KEY);
const upstream = await fetch(screenshotUrl.toString());
const buffer = Buffer.from(await upstream.arrayBuffer());
cache.set(cacheKey, buffer);
res.setHeader("Content-Type", "image/webp");
res.setHeader("Cache-Control", "public, max-age=3600");
res.setHeader("X-Cache", "MISS");
res.send(buffer);
} catch (err) {
res.status(500).json({ error: "Failed to capture preview" });
}
});
app.listen(3000);
WebP format is a good default here — it gives excellent compression at high quality, which keeps your preview images fast to load. On the client side, you can call /preview?url=https://example.com to get a thumbnail image for any URL.
For a production setup, replace the in-process NodeCache with Redis so previews persist across deployments and are shared across instances.
5. Visual Regression Testing
Visual regression testing means automatically comparing your UI before and after a code change to catch unintended visual breakage. This is especially valuable for component libraries, design systems, and apps with complex CSS.
The screenshot API fits naturally into a CI pipeline: capture baseline screenshots before the change, deploy, capture again, then diff.
Example: Visual regression in a Vitest test
// tests/visual/homepage.test.js
import { describe, it, expect } from "vitest";
import fetch from "node-fetch";
import { PNG } from "pngjs";
import pixelmatch from "pixelmatch";
import fs from "fs/promises";
import path from "path";
const API_KEY = process.env.LENSH_API_KEY;
const BASE_URL = process.env.PREVIEW_URL || "http://localhost:3000";
const SNAPSHOTS_DIR = path.resolve("tests/visual/snapshots");
async function screenshot(pagePath, width = 1280, height = 800) {
const url = `${BASE_URL}${pagePath}`;
const apiUrl = new URL("https://api.len.sh/v1/screenshot");
apiUrl.searchParams.set("url", url);
apiUrl.searchParams.set("width", String(width));
apiUrl.searchParams.set("height", String(height));
apiUrl.searchParams.set("format", "png");
apiUrl.searchParams.set("api_key", API_KEY);
const res = await fetch(apiUrl.toString());
return Buffer.from(await res.arrayBuffer());
}
describe("Visual regression: Homepage", () => {
it("matches the baseline snapshot", async () => {
const snapshotPath = path.join(SNAPSHOTS_DIR, "homepage.png");
const currentBuffer = await screenshot("/");
let baselineBuffer;
try {
baselineBuffer = await fs.readFile(snapshotPath);
} catch {
// No baseline yet — write it and pass
await fs.mkdir(SNAPSHOTS_DIR, { recursive: true });
await fs.writeFile(snapshotPath, currentBuffer);
return;
}
const baseline = PNG.sync.read(baselineBuffer);
const current = PNG.sync.read(currentBuffer);
const { width, height } = baseline;
const diff = new PNG({ width, height });
const numDiffPixels = pixelmatch(
baseline.data,
current.data,
diff.data,
width,
height,
{ threshold: 0.1 }
);
const diffRatio = numDiffPixels / (width * height);
expect(diffRatio).toBeLessThan(0.01); // less than 1% difference
});
});
In CI, you would:
- Check out the base branch, build the app, capture baseline screenshots
- Check out the feature branch, build, capture current screenshots
- Run
vitest— tests pass if visual differences are under the threshold
You can extend this to any number of pages. The len.sh API handles the browser, rendering, and network — your test just needs to do the diff.
Bonus: Thumbnail Galleries
If you are building a site directory, portfolio showcase, or curated link collection, you can use the screenshot API to automatically generate thumbnails for every entry. Kick off screenshot jobs when a new site is added, store the results in object storage (S3, R2, etc.), and serve them as thumbnails.
async function generateThumbnail(siteUrl) {
const apiUrl = `https://api.len.sh/v1/screenshot?url=${encodeURIComponent(siteUrl)}&width=800&height=600&format=webp&api_key=${process.env.LENSH_API_KEY}`;
const res = await fetch(apiUrl);
return res.arrayBuffer();
}
Pair this with a CDN and you have an auto-updating gallery of site screenshots that scales to thousands of entries without any manual work.
Getting Started with len.sh
All the examples above use the len.sh screenshot API. To get started:
- Sign up at len.sh and get your API key from the dashboard
- Make your first screenshot request:
curl "https://api.len.sh/v1/screenshot?url=https://example.com&api_key=YOUR_API_KEY" \
--output screenshot.png
The core screenshot parameters:
| Parameter | Description | Default |
|---|---|---|
url | The URL to screenshot (required) | — |
width | Viewport width in pixels | 1280 |
height | Viewport height in pixels | 800 |
format | Output format: png, jpeg, webp, pdf | png |
full_page | Capture full scrollable page (true/false) | false |
delay | Wait in milliseconds after page load | 0 |
api_key | Your API key (required) | — |
For branded OG images, use the /v1/og endpoint with these parameters:
| Parameter | Description | Default |
|---|---|---|
title | Main heading text (required) | — |
subtitle | Secondary text below the title | — |
badge | Small label displayed above the title | — |
url | URL displayed on the image | — |
brand_name | Your brand name | — |
brand_color | Hex color for brand accent (e.g. #4F46E5) | — |
theme | Color theme: light or dark | light |
api_key | Your API key (required) | — |
For full documentation, visit the len.sh docs.
Conclusion
Screenshot APIs have moved well beyond their “take a picture of a website” origins. They are now practical building blocks for dynamic image generation, monitoring infrastructure, document pipelines, link previews, and testing automation.
The five patterns above are starting points — most production implementations layer on caching, error handling, queuing, and storage. But the core integration is always just an HTTP call.
Pick the use case that fits your current problem, adapt the code example for your stack, and you can have a working integration in an afternoon. The browser management, rendering, and scaling are handled for you.
If you build something interesting with len.sh, we would love to hear about it. Reach out on GitHub or via the support page.