Screenshot API for Astro: Quick Start and Examples
Add website screenshots to Astro at build time or through an on-demand endpoint. This guide covers ScreenshotAPI, secure routing, caching, and a ScreenshotNeo one-call option.

To add a screenshot API to Astro, decide first whether each image should be generated during the build or in response to a live request. Use build-time generation for stable showcases and documentation. Use a server endpoint for dynamic or user-submitted URLs. Astro can do both: static endpoints run during the build, while server endpoints run when requested. In hybrid mode, mark a live route with export const prerender = false. Astro’s endpoint guide explains the distinction.
This guide shows a server endpoint using ScreenshotAPI’s Astro integration, followed by a build-time variant and practical safeguards. ScreenshotAPI (at screenshotapi.to) is a specific provider; its parameter names and authentication are not universal. It is separate from the similarly named Screenshot API at screenshot-api.org, which has a different host and API contract.
1. Choose when to capture
| Approach | Good fit | Trade-off |
|---|---|---|
| Build time | Showcases, docs, stable reference pages | Images update when you build and deploy again. |
| On demand | Changing pages or user-requested captures | Needs a server runtime, protected credentials, request controls, and a cache plan. |
Astro’s global fetch() follows the same timing distinction: in a statically generated site it runs during the build; with server-side rendering enabled it runs at runtime. Build-time data is fetched once for the deployed output unless you implement a client-side refresh. See Astro’s data fetching guide.

2. Create an on-demand screenshot endpoint
Configure the server and key
For a request-time endpoint, use Astro’s server output or hybrid output and install an adapter suitable for your deployment environment. The route below uses the provider guide’s API pattern: Astro’s built-in fetch(), a server-side SCREENSHOTAPI_KEY, and an x-api-key header. No additional HTTP package is needed.
Put the key in a local .env file and configure the same variable as a secret in your deployment platform:
SCREENSHOTAPI_KEY=your_screenshotapi_key
Do not put the key in a public client-side script or a variable intended for browser exposure. Add .env to your ignore rules if it is not already ignored.
Implement the route
Create src/pages/api/screenshot.ts. This example accepts a target URL plus a small set of capture options, checks the upstream status, and responds with image bytes and explicit headers. The exact provider parameters are specific to ScreenshotAPI’s guide.
import type { APIRoute } from 'astro';
export const prerender = false;
const allowedOutputs = new Set(['png', 'jpeg', 'webp']);
export const GET: APIRoute = async ({ request }) => {
const key = import.meta.env.SCREENSHOTAPI_KEY;
if (!key) {
return new Response('Screenshot service is not configured', { status: 500 });
}
const incoming = new URL(request.url);
const target = incoming.searchParams.get('url');
const width = incoming.searchParams.get('width') ?? '1280';
const height = incoming.searchParams.get('height') ?? '800';
const output = incoming.searchParams.get('output') ?? 'png';
const quality = incoming.searchParams.get('quality') ?? '80';
const colorScheme = incoming.searchParams.get('color_scheme') ?? 'light';
const fullPage = incoming.searchParams.get('full_page') ?? 'false';
if (!target) {
return new Response('Missing url query parameter', { status: 400 });
}
let targetUrl: URL;
try {
targetUrl = new URL(target);
} catch {
return new Response('url must be an absolute URL', { status: 400 });
}
if (!['http:', 'https:'].includes(targetUrl.protocol)) {
return new Response('Only http and https URLs are supported', { status: 400 });
}
if (!allowedOutputs.has(output)) {
return new Response('output must be png, jpeg, or webp', { status: 400 });
}
if (!/^\d+$/.test(width) || !/^\d+$/.test(height) ||
Number(width) < 1 || Number(height) < 1) {
return new Response('width and height must be positive integers', { status: 400 });
}
if (!['true', 'false'].includes(fullPage)) {
return new Response('full_page must be true or false', { status: 400 });
}
const apiUrl = new URL('https://screenshotapi.to/api/v1/screenshot');
apiUrl.searchParams.set('url', targetUrl.toString());
apiUrl.searchParams.set('width', width);
apiUrl.searchParams.set('height', height);
apiUrl.searchParams.set('output', output);
apiUrl.searchParams.set('quality', quality);
apiUrl.searchParams.set('color_scheme', colorScheme);
apiUrl.searchParams.set('full_page', fullPage);
let upstream: Response;
try {
upstream = await fetch(apiUrl, {
headers: { 'x-api-key': key },
signal: AbortSignal.timeout(60_000),
});
} catch {
return new Response('Screenshot provider could not be reached', { status: 502 });
}
if (!upstream.ok) {
return new Response('Screenshot provider returned an error', { status: 502 });
}
const bytes = await upstream.arrayBuffer();
const contentType = output === 'jpeg' ? 'image/jpeg' : `image/${output}`;
return new Response(bytes, {
status: 200,
headers: {
'Content-Type': contentType,
'Cache-Control': 'public, max-age=3600, s-maxage=3600',
'X-Content-Type-Options': 'nosniff',
},
});
};
Use the provider’s current integration guide to confirm its endpoint and accepted query fields before deploying; its API contract can change. The example returns generic upstream errors so that provider response details and credentials are not exposed to visitors. In production, log diagnostic details on the server without logging secrets.
Protect a public capture route
A route that accepts arbitrary URLs can trigger upstream work on your account. URL parsing and protocol checks are only a starting point. Decide which destinations the route is allowed to capture. If users only need screenshots of their own sites, consider an allowlist. Where arbitrary public URLs are required, block loopback, private-network, link-local, and internal hostnames after DNS resolution, and account for redirects that may lead to restricted destinations. Apply request limits and authentication or other abuse controls appropriate to your app.
These are deployment safeguards, not protections the provider example claims to supply. Do not pass a user-provided URL to a privileged service without choosing a target policy. Also validate numeric ranges and accepted option values; syntactically numeric dimensions can still be unreasonably large.
3. Generate stable screenshots during a build
For a fixed list of showcase sites, fetch captures while building and render them into the generated page. The following component shows the shape of that pattern. Keep the provider key in the build environment, and convert successful bytes to a data URL so the generated HTML can include the image without a runtime screenshot request.
---
// src/components/Showcase.astro
const sites = [
{ name: 'Astro', url: 'https://astro.build' },
{ name: 'Example', url: 'https://example.com' },
];
const captures = await Promise.all(sites.map(async (site) => {
try {
const endpoint = new URL('https://screenshotapi.to/api/v1/screenshot');
endpoint.searchParams.set('url', site.url);
endpoint.searchParams.set('width', '1200');
endpoint.searchParams.set('height', '800');
endpoint.searchParams.set('output', 'webp');
const response = await fetch(endpoint, {
headers: { 'x-api-key': import.meta.env.SCREENSHOTAPI_KEY },
});
if (!response.ok) throw new Error(`Provider status ${response.status}`);
const bytes = new Uint8Array(await response.arrayBuffer());
const binary = Array.from(bytes, (byte) => String.fromCharCode(byte)).join('');
const dataUrl = `data:image/webp;base64,${btoa(binary)}`;
return { ...site, dataUrl };
} catch (error) {
console.error(`Could not capture ${site.url}`, error);
return { ...site, dataUrl: null };
}
}));
---
<div class="showcase">
{captures.map((capture) => (
<figure>
<figcaption>{capture.name}</figcaption>
{capture.dataUrl
? <img src={capture.dataUrl} alt={`Screenshot of ${capture.name}`} loading="lazy" />
: <div class="placeholder">Preview unavailable</div>}
</figure>
))}
</div>
For a larger gallery, avoid embedding many large data URLs in HTML. Save generated files into a location your build publishes, or use a storage workflow suited to your deployment. Treat capture failure deliberately: fail the build if the images are required for correctness, or log the error and render a placeholder if the rest of the page should still publish.
A build-time capture can increase build duration and generated output size. It does not refresh after deployment until a new build runs. These are implementation trade-offs; there are no benchmark figures established here.
4. Useful variants
Generate an Open Graph image
A screenshot endpoint can also provide a social preview image. A common target canvas is 1200 × 630 pixels, but check the destination platform’s current image guidance and tailor the target page and capture settings. If the output is requested dynamically, return an image response from an on-demand route and choose caching based on whether the underlying page changes. If the image is stable, generate it during the build.
Capture light and dark themes
For a theme comparison, request the same URL twice with the provider’s color scheme option set to light and dark. Confirm the provider’s exact accepted values. If the site itself has a theme toggle rather than responding to a browser color preference, use an appropriate custom script or stylesheet only if the provider supports it, and verify the capture reflects the intended state.
Make a reusable gallery
Keep the URL list and presentation separate from the capture code. A component can receive records containing a label, destination, and generated image path. That makes it easier to rebuild only the captures that change and to display a useful placeholder when one source is unavailable.
5. Cache and operate the route
The sample response sets a one-hour public cache. That is a starting policy, not a universal setting. A rarely changing documentation page can use a longer lifetime; a frequently updated user preview should use a short lifetime or no shared cache. Include the target URL and every capture option that affects the pixels in the cache key. Otherwise a dark capture, for example, could be served to a light-mode request.
- Latency: A live request depends on your Astro server and the upstream capture. Use a request timeout and return a clear gateway error when the provider cannot respond.
- Reliability: Decide whether capture failures should fail the page, produce a placeholder, or return a retriable error. Log status and a request identifier where available, while keeping secrets and sensitive URLs out of logs.
- Cost: Each uncached upstream capture may consume provider quota or incur a charge under that provider’s terms. Cache repeat requests, limit dimensions and request rates, and check current plan details. Build-time captures consume quota when the build runs.
- Output size: Full-page captures may be much larger than viewport captures. Choose output format and quality according to use, and avoid returning unnecessary bytes to every visitor.
6. Troubleshooting
| Symptom | Likely cause | What to check |
|---|---|---|
| Route returns 404 in a static deployment | The route was prerendered or the deployment has no server runtime. | Use server output or hybrid mode with an adapter, then set prerender = false for the endpoint. |
| Missing key or provider authorization error | The secret is absent from the build or server environment, or the header is wrong. | Set SCREENSHOTAPI_KEY in the relevant environment and send it as x-api-key from server code. |
| Browser reports a CORS error | Client code is calling the provider directly. | Call your Astro server route from the browser and keep the provider key server-side. |
| Upstream returns an error | Invalid provider parameter, quota issue, rejected URL, or provider-side failure. | Inspect server-side status details and the provider’s current guide; return a controlled 502 to the caller. |
| Image is blank or wrong type | Response bytes or MIME type do not match the requested output, or capture failed upstream. | Check response.ok, provider response headers, and the Content-Type you return. |
| Old screenshot persists | Browser, CDN, or intermediary cache still has a prior response. | Adjust cache headers and cache key; account for the target URL and capture options. |
| Build breaks when one target is offline | A capture exception propagates from the build-time fetch. | Catch per-target errors and render a placeholder, or intentionally fail if every image is required. |

Or skip the browser setup
ScreenshotNeo provides a website screenshot API and MCP server. One GET request can return an image or PDF. Here is the one-call Node.js form:
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://astro.build' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`ScreenshotNeo returned ${res.status}`);
const bytes = new Uint8Array(await res.arrayBuffer());
await Bun.write('shot.webp', bytes);
For other environments, use the ScreenshotNeo docs for options and response details. Its cURL and Python calls are:
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://astro.build -o shot.webp
import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://astro.build"}, timeout=90)
open("shot.webp", "wb").write(r.content)
- Cookie banners are accepted and removed before the capture; known consent platforms, newsletter popups, and chat widgets can also be removed.
- Bot checks, blank pages, and failed loads are never billed. Response headers say what happened.
- An MCP server lets AI agents, including Claude and Cursor, take screenshots.
- 1,000 screenshots per month are free with no card; paid plans start at $5 for 3,000.
Sign up for ScreenshotNeo free: 1,000 screenshots a month, no card required.
FAQ
Can I use a screenshot endpoint on a static Astro site?
A static site can generate screenshots during its build. A live endpoint that captures a URL per request needs server rendering and a compatible deployment adapter.
Does Astro require a screenshot package?
The ScreenshotAPI Astro guide uses Astro’s built-in fetch() for its examples, so it does not require an additional HTTP package.
Will a build-time screenshot update when the target changes?
Not on its own. It updates when the site is rebuilt and the capture runs again.
Can I use parameters from another screenshot provider?
Do not assume so. Hosts, authentication methods, parameter names, response formats, and quotas belong to each provider’s own API contract.


