ScreenshotNeo

BlogHow-to

Test a Website Screenshot API with Your Web Framework

Add website screenshots to a web app with Playwright or an HTTP API. Learn how to capture, return, test, and troubleshoot screenshots safely.

By the ScreenshotNeo team4 October 202612 min read

To add website screenshots to an application, either run a browser on the server with Playwright or send a server-side HTTP request to a hosted screenshot API. Use Playwright when you need direct browser control or visual regression tests. Use an API when you want the browser rendering to sit outside your app’s runtime. In both cases, keep secrets on the server, validate target URLs, set timeouts, and check that the result is actually an image before returning it.

This guide uses Node.js for runnable examples and keeps the framework boundary generic: place the handler in a server-side route, controller, or job in your framework. The samples are integration patterns, not tested integrations for a named framework. Check your hosting platform’s current runtime constraints before deploying a browser process.

1. Choose where the browser runs

Approach Choose it when Tradeoff
Playwright in your server or test process You need browser interaction, custom setup, or a browser workflow already exists in the app. Your runtime must support the browser and its dependencies; you own browser lifecycle and capacity.
Hosted screenshot API An HTTP boundary is enough and you prefer not to run the browser in your app. You must handle credentials, network failures, provider limits, and provider-specific response formats.
Playwright Test screenshot assertion You need to detect visual changes during tests. It is a test assertion, not a way to serve screenshot bytes to application users.

There is no universally best route. Compare browser control, where rendering runs, target-page authentication, response format, failure handling, and usage terms. The reviewed sources do not provide a complete basis for comparing providers’ pricing, data retention, regional behavior, or contracts.

2. Capture a page with Playwright

Install Playwright and its Chromium browser in the environment where this server-side code will run. The exact browser installation command and runtime support depend on your deployment environment; follow the current Playwright installation guidance for that environment. Playwright’s Page API documents navigation and screenshot options.

const { chromium } = require('playwright');

async function capturePage(targetUrl) {
  const browser = await chromium.launch();
  try {
    const context = await browser.newContext({
      viewport: { width: 1440, height: 900 },
      deviceScaleFactor: 1
    });
    const page = await context.newPage();
    const response = await page.goto(targetUrl, {
      waitUntil: 'domcontentloaded',
      timeout: 30_000
    });

    if (!response) {
      throw new Error('Navigation did not return a main-document response');
    }
    if (!response.ok()) {
      throw new Error(`Target returned HTTP ${response.status()}`);
    }

    // Wait for a page-specific readiness signal when the site needs more time.
    await page.screenshot({ path: 'screenshot.png', fullPage: true });
    return { status: response.status(), finalUrl: page.url() };
  } finally {
    await browser.close();
  }
}

capturePage('https://example.com')
  .then(result => console.log(result))
  .catch(error => {
    console.error('Screenshot capture failed:', error.message);
    process.exitCode = 1;
  });

This saves a PNG to disk. To return or process image bytes instead, omit path and use the buffer returned by page.screenshot():

const image = await page.screenshot({ type: 'png', fullPage: true });
// Pass image to your framework's binary response or storage API.

Always close the browser in a finally block. In a long-running service, consider reusing a browser process and creating an isolated context per capture, then close each context when done. This can reduce repeated browser startup work, but you must monitor resource use and avoid sharing cookies or page state between requests.

Screenshot options to choose deliberately

  • fullPage: true captures the full scrollable page; without it, the screenshot is normally limited to the viewport.
  • clip: { x, y, width, height } captures a rectangular region. Use this when a specific area matters rather than the whole page.
  • type: 'png' | 'jpeg' selects an output format. JPEG supports a quality value; PNG is useful when you need lossless output or transparency.
  • scale: 'css' | 'device' controls whether output dimensions follow CSS pixels or device pixels. Device-scale output may use more memory and produce larger files.
  • Use a fixed viewport and deviceScaleFactor in the browser context when consistent dimensions matter.
  • Wait for a meaningful readiness condition. domcontentloaded means the document was parsed; it does not guarantee that client-rendered content or images have finished loading.

For dynamic pages, wait for a known selector, an application readiness marker, or a short deliberate delay. A blanket wait for all network activity can hang on pages that keep analytics or streaming requests open. Full-page screenshots can also be very tall and memory intensive. Use a viewport capture or clip when the complete page is unnecessary.

3. Expose capture through a server-side route

In a web framework, the handler should validate its input, call the capture function, and return a binary response with the correct content type. Framework response APIs differ, so adapt the final response call to your framework. Do not let a public endpoint accept arbitrary URLs without controls: it can be abused to make your server contact internal or sensitive network addresses. Allowlist permitted hosts where possible, reject non-HTTP(S) schemes, and enforce an outbound network policy.

// Framework-neutral handler shape. Adapt req/res to your framework.
async function screenshotHandler(req, res) {
  const targetUrl = req.query.url;
  let parsed;
  try {
    parsed = new URL(targetUrl);
  } catch {
    return res.status(400).json({ error: 'A valid URL is required' });
  }
  if (!['http:', 'https:'].includes(parsed.protocol)) {
    return res.status(400).json({ error: 'Only HTTP and HTTPS URLs are allowed' });
  }

  try {
    const browser = await chromium.launch();
    let image;
    try {
      const page = await browser.newPage({ viewport: { width: 1440, height: 900 } });
      const response = await page.goto(parsed.href, {
        waitUntil: 'domcontentloaded', timeout: 30_000
      });
      if (!response || !response.ok()) {
        return res.status(502).json({ error: 'The target page did not load successfully' });
      }
      image = await page.screenshot({ type: 'png' });
    } finally {
      await browser.close();
    }
    res.setHeader('Content-Type', 'image/png');
    res.setHeader('Cache-Control', 'no-store');
    return res.status(200).send(image);
  } catch (error) {
    console.error('Screenshot route failed', error);
    return res.status(502).json({ error: 'Screenshot capture failed' });
  }
}

The sample shows the control flow, not a drop-in route for every framework. In production, ensure the browser is closed even when navigation or screenshot creation throws, return only safe error details to clients, and set a request-level deadline that leaves time to send an error response. Consider moving slow or full-page captures into a background job rather than tying up a web request.

4. Call a hosted screenshot API from the server

A hosted API keeps browser execution outside your application. The app sends the target URL and capture options, then either forwards the returned image bytes or stores them and returns an application-controlled URL. Keep the API key in a server-side environment variable or secret store. Never place it in browser JavaScript, a public page, or a client-visible URL.

Providers vary: some return raw image bytes, while others may return structured data or a URL. Decide which response your application expects and inspect the response status and content type before treating the body as an image. For example, Screenshot API documents bearer-token authentication and errors including unauthorized (401), invalid request (400), rate limited and quota exceeded (429), render failed (502), and selector not found (422). Its documentation lists free-plan limits of 60 requests per minute and 500 screenshots per month; these are provider-stated limits and can change. See its REST documentation. screenshot-api.net documents raw image-byte output and response headers for quota, render time, and final page status; consult its documentation for that provider’s behavior.

Example HTTP request patterns

The following generic examples show the shape of a server-to-service image request. Replace the endpoint, authentication, parameters, and expected response with those documented by your chosen provider. Do not assume that one provider’s parameter names or response headers work with another.

curl -G 'https://SCREENSHOT_API_HOST/ENDPOINT' \
  -H 'Authorization: Bearer YOUR_API_KEY' \
  --data-urlencode 'url=https://example.com' \
  --output screenshot.png
import requests

response = requests.get(
    'https://SCREENSHOT_API_HOST/ENDPOINT',
    headers={'Authorization': 'Bearer YOUR_API_KEY'},
    params={'url': 'https://example.com'},
    timeout=90,
)
response.raise_for_status()
content_type = response.headers.get('content-type', '')
if not content_type.startswith('image/'):
    raise RuntimeError(f'Expected image response, got {content_type!r}')
with open('screenshot.png', 'wb') as output:
    output.write(response.content)
const endpoint = new URL('https://SCREENSHOT_API_HOST/ENDPOINT');
endpoint.searchParams.set('url', 'https://example.com');
const response = await fetch(endpoint, {
  headers: { Authorization: `Bearer ${process.env.SCREENSHOT_API_KEY}` },
  signal: AbortSignal.timeout(90_000)
});
if (!response.ok) {
  throw new Error(`Screenshot API returned HTTP ${response.status}`);
}
const contentType = response.headers.get('content-type') || '';
if (!contentType.startsWith('image/')) {
  throw new Error(`Expected image response, got ${contentType}`);
}
const image = Buffer.from(await response.arrayBuffer());
// Return image using your framework's binary response API.

5. Test the integration and visual output

Separate API integration checks from visual regression checks. An integration check should confirm that your route responds with the expected status, content type, and non-empty body. It should also verify error mapping for an invalid URL and a failed capture. Use a stable test page that you control rather than relying on a third-party site’s content remaining unchanged.

For visual regression, Playwright Test provides toHaveScreenshot(). It waits for two consecutive screenshots to stabilize before comparing the result with the expectation, and the assertion is limited to the Playwright test runner. See the PageAssertions API. Keep the viewport, fonts, data, locale, and other page state stable; dynamic timestamps, rotating content, animation, and remote images can create diffs unrelated to your code.

const { test, expect } = require('@playwright/test');

test('landing page visual baseline', async ({ page }) => {
  await page.setViewportSize({ width: 1440, height: 900 });
  await page.goto('https://example.com');
  await expect(page).toHaveScreenshot('landing-page.png');
});

6. Add capture options without making results unpredictable

Start with the smallest capture that answers the product need, then add options only when the use case requires them.

Need Playwright approach Hosted API consideration
Entire document fullPage: true; be mindful of tall-page memory use. Check whether full-page capture is supported and any size constraints.
One region or component Use clip coordinates or a locator screenshot. Use the provider’s documented selector or element option if available.
Consistent viewport Set viewport and device scale factor on the context. Send provider-documented viewport/device parameters.
Page needs time to render Wait for a selector or explicit page-ready condition. Use a documented wait condition or delay where supported.
Authenticated target Set context cookies or credentials carefully. Use only documented target-page authentication options; protect credentials.
Image size or format Choose PNG/JPEG, quality where supported, and CSS/device scale. Verify supported formats, response type, and resizing behavior.

Do not treat a successful HTTP status as proof that the intended page rendered. A service may successfully return a screenshot of an authentication page, access-denied page, or other error document. Where available, inspect final page status or page-verdict metadata and validate the result for your use case.

7. Or skip the browser setup

ScreenshotNeo is a website screenshot API and MCP server. Its documentation describes a single GET request that returns a screenshot or PDF. Example using 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}`);
  • Cookie and consent banners are accepted like a visitor, and 60+ known consent platforms, newsletter popups, and chat widgets are removed before capture. Each step can be turned off.
  • Bot checks and CAPTCHAs, blank pages, timeouts, failed loads, and cache hits cost nothing; responses say which outcome occurred in X-Page-Verdict and X-Billed headers.
  • An MCP server provides take_screenshot, get_page_info, and capture_pdf tools for AI agents and MCP clients.
  • The free plan includes 1,000 screenshots a month with no card; paid plans start at $5 for 3,000 screenshots. Yearly billing gives two months free, and every feature is on every plan.

Sign up for ScreenshotNeo’s free plan to get 1,000 screenshots a month with no card.

8. Troubleshooting common failures

Symptom Likely cause Fix
Browser launch fails in deployment The runtime image lacks browser binaries or required system dependencies, or the host restricts browser processes. Install the browser and dependencies supported by the host, or use a hosted API. Confirm current runtime constraints before deployment.
Navigation times out The target is slow, never reaches the chosen wait condition, or holds network connections open. Use a bounded timeout, choose a narrower readiness signal such as domcontentloaded plus a selector, and report a clear timeout error.
Screenshot is blank or incomplete Client-side content has not rendered, lazy content is not loaded, or the site returned a blank/error page. Wait for the page’s real ready state, inspect the final URL and response, and test against a controlled page. Avoid assuming navigation alone proves content is ready.
API returns 401 Missing, malformed, expired, or incorrectly scoped API credentials. Check server-side secret configuration and the provider’s required authentication format. Do not expose keys to browsers or logs.
API returns 400 or 422 Malformed parameters or a selector that was not found. Validate inputs before sending; confirm option names and selector behavior in the provider’s documentation.
API returns 429 Rate limit or quota reached. Read provider quota headers if supplied, queue work, limit concurrency, and retry only when appropriate with backoff.
API returns 502 or an error page image Rendering failed, or the target itself displayed an access or authentication error page. Check provider error details and final-page metadata; verify target access and do not present an error-page image as a successful capture.
Screenshot tests fail intermittently Dynamic content, animations, fonts, timing, or external resources differ between runs. Fix test data and page state, wait for stable content, and reduce reliance on uncontrolled third-party resources.
Image body is actually JSON or HTML The request failed or the service returns structured output rather than raw bytes. Check status and content type before saving or forwarding the body; parse structured errors separately.

9. Performance, reliability, and cost

Performance

  • Use a fixed viewport and capture only the region needed. Full-page and device-scale captures can produce much larger images.
  • Reuse a browser process for repeated local captures when your runtime supports it, while isolating each request in its own context and bounding concurrency.
  • Set navigation, capture, and overall request deadlines. Avoid holding a user-facing request open for work that can run as a background job.
  • Use provider caching or application caching only when freshness requirements allow it. A cached image may not represent the page’s current state.

Reliability

  • Validate URLs and restrict destinations to reduce server-side request risks.
  • Handle navigation errors, non-success target responses, provider errors, and unexpected response types distinctly.
  • Do not retry every failure. Invalid input, authorization errors, missing selectors, and quota errors need a correction or quota recovery; transient network failures may merit a bounded retry with backoff.
  • Log request identifiers, duration, status, and outcome where available. Avoid logging API keys, cookies, authorization headers, or sensitive target URLs.

Cost

Local Playwright shifts costs to your own compute, storage, and operational maintenance; the reviewed sources do not establish a price comparison. Hosted service pricing and quotas are provider-specific and can change. Check current plan limits and terms before relying on them. ScreenshotNeo states that only clean shots are billed and lists free and paid allowances in the product section above. For any provider, estimate capture volume, average image size, full-page frequency, retry behavior, and cache reuse before setting a budget.

10. Frequently asked questions

Can I use a screenshot API from frontend code?

Keep the provider key on your server. Have browser code call your own authenticated backend route instead of sending a secret to a third-party screenshot endpoint.

Does a screenshot prove the page works?

No. It proves that an image was produced. Pair the capture with status checks and assertions about the expected page content or state.

Should I use screenshot capture for visual tests?

Use a test runner assertion such as Playwright Test’s screenshot matcher when the goal is comparing a page against a visual baseline. A hosted API may provide an image, but that alone does not define a test’s comparison or acceptance behavior.

What image format should I return?

Use PNG when lossless detail or transparency matters, and JPEG when lossy compression is acceptable. Confirm that the browser or API response actually matches the content type you send to clients.

Sources