How to Use ScreenshotOne in a Next.js App
Call ScreenshotOne from a Next.js server route, protect your API key, validate screenshot requests, and return image bytes with the right headers.
Use ScreenshotOne from server-side code in your Next.js app. Store the access key in a server-only environment variable, validate the page URL and allowed options, call ScreenshotOne’s /take endpoint over HTTPS, and return the binary image with its content type. This keeps the key out of browser code and gives your app a place to control usage.
This guide uses an App Router Route Handler. Route Handlers live in the app directory and use the Web Request and Response APIs; check the documentation for your installed Next.js version because the linked framework reference is for Next.js 13. Next.js Route Handlers
1. Create a ScreenshotOne key and configure Next.js
- Create an access key in ScreenshotOne. The access key authenticates API requests; the secret key is a separate credential used for signing or webhook verification. ScreenshotOne API keys
- Add the access key to your local environment file, such as
.env.local:
SCREENSHOTONE_ACCESS_KEY=your_access_key
Do not prefix this variable with NEXT_PUBLIC_. Next.js exposes variables with that prefix to browser code. Keep the file out of source control, and use your deployment platform’s secret configuration in production. ScreenshotOne advises treating the key like a password. API key guidance
Use HTTPS for requests. ScreenshotOne notes that HTTP does not encrypt keys, authorization headers, or cookies in transit. Getting Started
2. Add a server-side screenshot route
Create app/api/screenshot/route.ts. This example accepts a URL and a small, explicit set of image options. It validates the URL, calls ScreenshotOne, handles JSON errors, and streams the returned bytes to the caller.
import { NextRequest, NextResponse } from "next/server";
const allowedHosts = new Set(["example.com", "www.example.com"]);
function isAllowedTarget(raw: string): URL | null {
try {
const url = new URL(raw);
if (url.protocol !== "https:") return null;
if (!allowedHosts.has(url.hostname)) return null;
if (url.username || url.password) return null;
return url;
} catch {
return null;
}
}
export async function GET(request: NextRequest) {
const accessKey = process.env.SCREENSHOTONE_ACCESS_KEY;
if (!accessKey) {
return NextResponse.json(
{ error: "Screenshot service is not configured" },
{ status: 500 },
);
}
const rawTarget = request.nextUrl.searchParams.get("url");
if (!rawTarget) {
return NextResponse.json({ error: "Missing url parameter" }, { status: 400 });
}
const target = isAllowedTarget(rawTarget);
if (!target) {
return NextResponse.json({ error: "URL is not allowed" }, { status: 400 });
}
const params = new URLSearchParams({
access_key: accessKey,
url: target.toString(),
format: "png",
});
let upstream: Response;
try {
upstream = await fetch(`https://api.screenshotone.com/take?${params}`, {
signal: AbortSignal.timeout(90_000),
});
} catch {
return NextResponse.json(
{ error: "Could not reach the screenshot service" },
{ status: 502 },
);
}
if (!upstream.ok) {
const payload = await upstream.json().catch(() => null);
const message = payload?.error?.message ?? "Screenshot request failed";
return NextResponse.json({ error: message }, { status: upstream.status });
}
const contentType = upstream.headers.get("content-type") ?? "image/png";
const bytes = await upstream.arrayBuffer();
return new Response(bytes, {
status: 200,
headers: {
"Content-Type": contentType,
"Cache-Control": "no-store",
},
});
}
Replace the example host allowlist with the domains your application actually needs. If the route is meant to capture arbitrary public URLs, validate the protocol and reject credentials, then add authentication, rate limiting, and appropriate destination controls. A public endpoint that accepts arbitrary URLs can be misused to make requests you did not intend. Restrict which screenshot options callers can set as well.
The handler uses AbortSignal.timeout to cap how long it waits for the upstream request. Confirm runtime support for your Next.js deployment target. If your runtime does not support it, use the timeout mechanism supported by that runtime.
3. Call the route from a page
The browser calls your own route; the ScreenshotOne key stays on the server. For example, a client component can render the returned image as a blob URL:
"use client";
import { useState } from "react";
export function ScreenshotPreview() {
const [imageUrl, setImageUrl] = useState<string | null>(null);
const [error, setError] = useState<string | null>(null);
async function capture() {
setError(null);
setImageUrl(null);
const response = await fetch(
"/api/screenshot?url=" + encodeURIComponent("https://example.com"),
);
if (!response.ok) {
const body = await response.json().catch(() => null);
setError(body?.error ?? "Screenshot request failed");
return;
}
const blob = await response.blob();
setImageUrl(URL.createObjectURL(blob));
}
return (
<section>
<button onClick={capture}>Capture page</button>
{error && <p role="alert">{error}</p>}
{imageUrl && <img src={imageUrl} alt="Website screenshot" />}
</section>
);
}
For a long-lived component that replaces screenshots repeatedly, revoke prior blob URLs with URL.revokeObjectURL when they are no longer used. If you want an image URL usable by a normal <img> tag without a browser blob step, consider a carefully designed server-side cache or a signed URL. Do not return an unsigned ScreenshotOne URL containing the access key to the browser.
4. Direct API request options
ScreenshotOne supports HTTPS GET and POST requests. GET is convenient for ordinary URLs and options. Use POST JSON for large HTML or Markdown input instead of putting large content in a query string; the documented maximum request body is 100 MiB. Getting Started
For a direct GET request, the essential parameters are the target url and access_key. Add documented screenshot options such as format as needed. Consult the live ScreenshotOne options reference for the full option list and accepted values; validate and allowlist options in your own route rather than forwarding arbitrary user parameters.
cURL
curl -G "https://api.screenshotone.com/take" \
--data-urlencode "access_key=$SCREENSHOTONE_ACCESS_KEY" \
--data-urlencode "url=https://example.com" \
--data-urlencode "format=png" \
--output screenshot.png
Python
import os
import requests
response = requests.get(
"https://api.screenshotone.com/take",
params={
"access_key": os.environ["SCREENSHOTONE_ACCESS_KEY"],
"url": "https://example.com",
"format": "png",
},
timeout=90,
)
if not response.ok:
try:
detail = response.json()
except ValueError:
detail = response.text
raise RuntimeError(f"ScreenshotOne error ({response.status_code}): {detail}")
with open("screenshot.png", "wb") as output:
output.write(response.content)
Node.js
const params = new URLSearchParams({
access_key: process.env.SCREENSHOTONE_ACCESS_KEY,
url: "https://example.com",
format: "png",
});
const response = await fetch(`https://api.screenshotone.com/take?${params}`, {
signal: AbortSignal.timeout(90_000),
});
if (!response.ok) {
const detail = await response.json().catch(() => null);
throw new Error(detail?.error?.message ?? `ScreenshotOne returned ${response.status}`);
}
const image = Buffer.from(await response.arrayBuffer());
await import("node:fs/promises").then(({ writeFile }) =>
writeFile("screenshot.png", image),
);
These examples save or return binary response data; do not treat an image response as JSON. On errors, ScreenshotOne documents a JSON error with an error code and message. Getting Started
5. Use the official JavaScript and TypeScript SDK
If you prefer the typed client over constructing requests yourself, install ScreenshotOne’s SDK:
npm install screenshotone-api-sdk
The documented SDK pattern creates a Client with access and secret keys, creates options with TakeOptions.url(...), and calls client.take(options). Recent SDK methods are asynchronous. Follow the SDK’s current examples for imports and exact option types:
import * as screenshotOne from "screenshotone-api-sdk";
const client = new screenshotOne.Client(
process.env.SCREENSHOTONE_ACCESS_KEY!,
process.env.SCREENSHOTONE_SECRET_KEY!,
);
const options = screenshotOne.TakeOptions.url("https://example.com")
.format("png");
const response = await client.take(options);
const bytes = await response.arrayBuffer();
Keep both credentials server-side. When a URL must be shared, use the SDK’s generateSignedTakeURL() method as documented instead of exposing an ordinary generated URL that includes the access key. JavaScript and TypeScript SDK
6. Handle security, output formats, and special inputs
Validate user-provided URLs
- Allow only protocols your app needs, typically
https:. - Consider an explicit hostname allowlist when the app has a known set of targets.
- Reject URL usernames and passwords, malformed inputs, and options your app does not support.
- Protect the route with your normal authorization and application-level rate limits if callers can trigger billable captures.
Return the correct response type
The API can return image formats, PDF, HTML, or Markdown depending on the requested options. Preserve the upstream Content-Type rather than always labeling the result as PNG. Handle an unsuccessful status as an error response, not as image bytes. Screenshot options
Large HTML or Markdown input
When capturing supplied HTML or Markdown, send it in a POST JSON body. ScreenshotOne documents a 100 MiB maximum body size. A practical application should usually impose a lower limit based on its own request, memory, and execution budgets. Getting Started
Authenticated pages
ScreenshotOne documents authorization headers and cookies for pages you own or are permitted to access. Obtaining session cookies may require custom sign-in code. Do not accept arbitrary user credentials or forward session cookies through a generic screenshot endpoint; design that flow explicitly and protect secrets. Screenshot authenticated pages
7. Performance, reliability, and cost
- Bound the wait. Set an application timeout appropriate to your runtime and report timeout failures clearly. Avoid letting a request occupy a serverless function indefinitely.
- Cache when appropriate. If the target and capture options are identical and freshness requirements allow it, cache the result in your application. Include all output-affecting options in the cache key, and choose a TTL that matches the page’s update rate.
- Keep payloads controlled. Large HTML inputs and large screenshot responses consume bandwidth and memory. Set application limits and avoid unnecessary conversions or duplicate buffers.
- Retry selectively. A timeout or transient upstream failure may be retriable, but do not retry every error automatically. Use bounded retries with backoff, and avoid multiplying billable requests or delaying the caller.
- Watch usage. Require authorization or rate limits if users can submit captures, and surface upstream errors to logs without logging access keys, cookies, or authorization values.
ScreenshotOne’s pricing and account limits are not specified in the research used for this guide, so check its current account and pricing documentation before estimating production cost. The documented 100 MiB figure is a maximum POST body size, not a recommended payload size.
8. Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| Your route returns “Screenshot service is not configured.” | The environment variable is missing or was added after the dev server started. | Set SCREENSHOTONE_ACCESS_KEY in the server environment and restart the server. Configure it in the deployment environment too. |
| The upstream request returns an authentication error. | The access key is missing, invalid, rotated, or confused with the separate secret key. | Check the access key in the ScreenshotOne account and keep the access and secret keys in their intended roles. Do not expose either in browser code. |
| The browser receives JSON where it expected an image. | The upstream API returned an error, or the route did not check upstream.ok. |
Parse and return the JSON error with a non-success status before reading a successful response as bytes. |
| The downloaded file cannot be opened or has the wrong type. | The response was saved with the wrong extension or a fixed content type was used despite requesting another format. | Check the requested format and preserve the upstream Content-Type. Match the file extension to the selected format. |
| The route rejects a URL that should be capturable. | The application allowlist does not include its hostname, the URL is not HTTPS, or the input is malformed. | Inspect the parsed hostname and protocol. Adjust the allowlist only for destinations your app intends to permit. |
| Captures time out or the page appears incomplete. | The target page may load slowly, or the application timeout may be too short. | Use the relevant wait options from the ScreenshotOne options reference, increase the bounded timeout if your runtime allows it, and handle timeout errors explicitly. |
| A shared URL exposes a credential. | An unsigned generated URL contains an access key. | Do not publish it. Use the SDK’s signed URL method for URLs that must be shared, and rotate a compromised key. |
| Large HTML or Markdown requests fail. | Large content was put in a GET query string, exceeded a request limit, or consumed too much memory. | Use POST JSON for large content, stay below the documented 100 MiB maximum, and enforce a sensible lower application limit. |
Or skip the browser setup
ScreenshotNeo is a website screenshot API and MCP server for developers. Its one-call endpoint returns an image or PDF, and its parameter names also work with those used by other screenshot APIs, which can simplify a switch. Learn about ScreenshotNeo.
Cookie banners, newsletter popups, and chat widgets are removed before the shot; each cleanup step can be turned off. Bot checks, blank pages, timeouts, failed loads, and cache hits cost nothing, with page verdict and billing details in response headers. Its MCP server lets Claude, Cursor, and other MCP clients use take_screenshot, get_page_info, and capture_pdf. The free plan includes 1,000 shots per month with no card; paid plans start at $5 for 3,000 shots. Every feature is on every plan.
See the ScreenshotNeo API documentation for request options.
cURL
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
Python
import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"}, timeout=90)
open("shot.webp", "wb").write(r.content)
Node.js
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
Create a free ScreenshotNeo account for 1,000 screenshots a month with no card.
FAQ
Can I call ScreenshotOne from a client component?
Make the request through a server route or another server-side layer so the access key is not shipped to the browser.
Do I need the secret key for a screenshot request?
The access key authenticates API requests. The secret key is separate and is used for signing or webhook verification; follow the SDK documentation for features that require it.
Can the route return a PDF instead of an image?
Yes. Request the relevant documented output option and return the upstream body with its actual content type. Ensure the client handles a PDF response rather than rendering it as an image.
Is the example suitable for every Next.js runtime?
It uses standard Request and Response patterns, but runtime support and framework details can vary. Check the documentation for the version and deployment runtime used by your app.


