ScreenshotNeo

BlogHow-to

How to Use URLbox to Capture a Website Screenshot from a Chrome Extension

Capture the active tab with URLbox from a Chrome extension using a backend to protect your API secret, choose capture options, and return the image safely.

By the ScreenshotNeo team4 October 202612 min read

Use the Chrome extension to read the active tab URL after the user clicks, send that URL and a small set of permitted capture options to your own HTTPS backend, and have the backend call URLbox. Keep the URLbox project secret on the backend. Return the image bytes or a temporary render URL to the extension. URLbox documents its render API, but its reviewed documentation does not provide a Chrome-extension-specific manifest or tutorial; the extension-to-backend pattern below is an implementation recommendation based on its API and credential model.

URLbox accepts render options through render links or its JSON API. Its JSON API uses Bearer authentication at https://api.urlbox.com. The synchronous endpoint is /v1/render/sync; the asynchronous endpoint is /v1/render/async. URLbox Quick Start · API reference.

1. Choose the extension and backend responsibilities

  1. The user invokes a toolbar action or context-menu command.
  2. The extension reads the active tab URL and sends it with a constrained capture request to your backend over HTTPS.
  3. Your backend validates the request and URL, applies server-owned policy, then calls URLbox with its secret.
  4. The backend returns an image or a URL to the extension UI. Persist the image or configure cloud storage if it must remain available beyond the temporary URL lifetime.

Do not place the URLbox secret in extension JavaScript, a manifest, a bundled environment file, or a signed-in user’s browser storage. Distributed extension code can be inspected. URLbox documents both Bearer-authenticated JSON requests and HMAC-SHA256 signatures for secure render links; if using a signed link, have trusted server code sign only narrowly allowed options. See the quickstart and API reference.

2. Set up a minimal Chrome extension

This Manifest V3 example uses a toolbar click and a content script to show the returned image in the current page. Replace https://app.example.com with your backend origin. The host permission is for your backend, not for every site being captured: the extension reads the tab URL through the user-invoked action, while URLbox fetches the target page server-side. Exact permission needs depend on the extension’s design; these entries are not a URLbox-prescribed manifest.

{
  "manifest_version": 3,
  "name": "Page Screenshot",
  "version": "1.0.0",
  "permissions": ["activeTab"],
  "host_permissions": ["https://app.example.com/*"],
  "background": {"service_worker": "background.js", "type": "module"},
  "content_scripts": [{
    "matches": ["<all_urls>"],
    "js": ["content.js"]
  }],
  "action": {"default_title": "Capture this page"}
}

background.js obtains the current tab only when the user clicks. It sends JSON to the backend, checks for an HTTP error, then forwards the returned image data to the content script. The example expects the backend response shown in the next section.

chrome.action.onClicked.addListener(async (tab) => {
  if (!tab.id || !tab.url) return;
  let page;
  try {
    page = new URL(tab.url);
  } catch {
    return;
  }
  if (!['http:', 'https:'].includes(page.protocol)) return;

  try {
    const response = await fetch('https://app.example.com/api/screenshot', {
      method: 'POST',
      headers: {'Content-Type': 'application/json'},
      body: JSON.stringify({
        url: page.href,
        options: {width: 1280, height: 800, format: 'png'}
      })
    });
    if (!response.ok) throw new Error(`Backend returned ${response.status}`);
    const result = await response.json();
    await chrome.tabs.sendMessage(tab.id, {
      type: 'screenshot-result',
      dataUrl: `data:image/${result.format};base64,${result.imageBase64}`
    });
  } catch (error) {
    await chrome.tabs.sendMessage(tab.id, {
      type: 'screenshot-error',
      message: error.message
    }).catch(() => {});
  }
});

content.js displays the image. A production extension should replace the basic overlay with its own popup or side panel, and should provide a clear loading and error state.

chrome.runtime.onMessage.addListener((message) => {
  if (message.type === 'screenshot-result') {
    const image = document.createElement('img');
    image.src = message.dataUrl;
    image.alt = 'Screenshot of the current page';
    Object.assign(image.style, {
      position: 'fixed', zIndex: '2147483647', right: '16px', top: '16px',
      maxWidth: 'min(480px, 90vw)', maxHeight: '80vh',
      border: '1px solid #888', background: 'white'
    });
    document.body.append(image);
  } else if (message.type === 'screenshot-error') {
    console.error('Screenshot failed:', message.message);
  }
});

Content scripts do not run on every browser-internal or restricted page, and they may not be available on the tab that was captured. For a robust interface, open an extension-owned page or popup and pass the result there instead of relying on a content script.

3. Add a backend that calls URLbox

The following Node.js/Express service accepts only url, width, height, format, full_page, and selector. Add authentication, per-user rate limits, request-size limits, and application-specific URL policy before exposing it publicly. The sample returns base64 image data for clarity; large images are usually better streamed or stored and represented by a short-lived application URL.

import express from 'express';

const app = express();
app.use(express.json({limit: '16kb'}));

const allowedFormats = new Set(['png', 'jpeg', 'webp']);

app.post('/api/screenshot', async (req, res) => {
  try {
    const {url, options = {}} = req.body ?? {};
    const target = new URL(url);
    if (!['http:', 'https:'].includes(target.protocol)) {
      return res.status(400).json({error: 'Only HTTP and HTTPS URLs are supported'});
    }

    const width = Number(options.width ?? 1280);
    const height = Number(options.height ?? 800);
    const format = options.format ?? 'png';
    if (!Number.isInteger(width) || width < 1 || width > 4096 ||
        !Number.isInteger(height) || height < 1 || height > 4096) {
      return res.status(400).json({error: 'Invalid viewport dimensions'});
    }
    if (!allowedFormats.has(format)) {
      return res.status(400).json({error: 'Unsupported image format'});
    }

    const renderOptions = {url: target.href, width, height, format};
    if (options.full_page === true) renderOptions.full_page = true;
    if (typeof options.selector === 'string' && options.selector.length <= 256) {
      renderOptions.selector = options.selector;
    }

    const upstream = await fetch('https://api.urlbox.com/v1/render/sync', {
      method: 'POST',
      headers: {
        'Authorization': `Bearer ${process.env.URLBOX_SECRET}`,
        'Content-Type': 'application/json'
      },
      body: JSON.stringify(renderOptions),
      signal: AbortSignal.timeout(90000)
    });
    if (!upstream.ok) {
      const detail = await upstream.text();
      console.error('URLbox request failed:', upstream.status, detail);
      return res.status(502).json({error: 'Screenshot provider request failed'});
    }

    const contentType = upstream.headers.get('content-type') ?? '';
    if (contentType.includes('application/json')) {
      const result = await upstream.json();
      return res.json({renderUrl: result.renderUrl, format});
    }
    const bytes = Buffer.from(await upstream.arrayBuffer());
    return res.json({format, imageBase64: bytes.toString('base64')});
  } catch (error) {
    console.error('Screenshot request error:', error);
    return res.status(500).json({error: 'Could not capture screenshot'});
  }
});

app.listen(process.env.PORT ?? 3000);

Set URLBOX_SECRET in the hosting platform’s secret manager, not in source control. The URLbox sync response may be a JSON object containing a renderUrl; depending on the request style and response, handle either returned JSON metadata or image bytes. For a deployment, verify the response content type and current account/API behavior against the official quickstart before selecting the response path.

4. Make direct API requests from trusted code

These examples call URLbox directly and therefore belong in your backend, a private script, or another trusted environment. They are not safe to bundle into a public extension because they require the project secret. The sync API documentation shows JSON request options and a response that can include a temporary renderUrl; render-link requests can return image data directly. Confirm the response mode you choose and relay it consistently to the extension.

cURL

curl -X POST 'https://api.urlbox.com/v1/render/sync' \
  -H "Authorization: Bearer $URLBOX_SECRET" \
  -H 'Content-Type: application/json' \
  -d '{"url":"https://example.com","width":1280,"height":800,"format":"png"}'

Python

import os
import requests

response = requests.post(
    "https://api.urlbox.com/v1/render/sync",
    headers={
        "Authorization": f"Bearer {os.environ['URLBOX_SECRET']}",
        "Content-Type": "application/json",
    },
    json={
        "url": "https://example.com",
        "width": 1280,
        "height": 800,
        "format": "png",
    },
    timeout=90,
)
response.raise_for_status()

content_type = response.headers.get("content-type", "")
if "application/json" in content_type:
    result = response.json()
    print("Temporary render URL:", result.get("renderUrl"))
else:
    with open("screenshot.png", "wb") as image:
        image.write(response.content)

Node.js

const response = await fetch('https://api.urlbox.com/v1/render/sync', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.URLBOX_SECRET}`,
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    url: 'https://example.com', width: 1280, height: 800, format: 'png'
  }),
  signal: AbortSignal.timeout(90000)
});
if (!response.ok) throw new Error(`URLbox returned ${response.status}`);
const type = response.headers.get('content-type') ?? '';
if (type.includes('application/json')) {
  const result = await response.json();
  console.log('Temporary render URL:', result.renderUrl);
} else {
  const image = Buffer.from(await response.arrayBuffer());
  await import('node:fs/promises').then(fs => fs.writeFile('screenshot.png', image));
}

5. Select the screenshot scope and options

Need Option Guidance
Browser-sized viewport capture width, height, format Viewport dimensions are in pixels. Choose dimensions appropriate to the preview shown in the extension.
Whole scrolling page full_page: true Default stitch mode favors accuracy and scrolls to trigger lazy content. Native mode favors speed but may be less accurate on some sites.
One page component selector Pass a CSS selector, such as #pricing. Find it in Chrome DevTools by inspecting the element and choosing Copy selector.
Horizontal page content full_width: true Relevant to full-page stitch captures when content scrolls horizontally.
Known long or infinite page max_sections, allow_infinite Limit stitched sections where suitable. Infinite scrolling is guarded by default; enable continued scrolling only when intended.
Skip lazy-load preparation skip_scroll: true Can save time, but content that appears only after scrolling may be missing.

The full-page guide describes the default stitch mode as accuracy-oriented and native as faster but less reliable across sites. Test representative target pages. Large images have format-specific limits: the docs list maximum dimensions of 65,535 × 65,535 pixels for JPEG and 16,383 × 16,383 for WebP; they recommend PNG for full-page captures without those stated limits. See Screenshots and Render Options.

6. Choose synchronous or asynchronous rendering

Use /v1/render/sync for a simple user-triggered capture where the extension can wait and display a spinner. Use /v1/render/async when renders may take longer, you need to process work in the background, or you want to handle parallel jobs. The async endpoint returns a render ID and status URL; your backend can poll or use a webhook. Do not make the extension poll URLbox directly with credentials.

The quickstart says a JSON response’s renderUrl expires after 30 days. Download and store the image or configure cloud storage if the user expects durable access. For short-lived previews, return the URL only if it is acceptable for it to expire. The API’s endpoints and async response are documented in the API reference.

7. Validate requests and protect your backend

  • Require a signed-in session or another authorization mechanism before accepting renders; otherwise your endpoint can be used by strangers at your expense.
  • Allow only http: and https:, and decide whether to reject local, private, and infrastructure addresses. A screenshot endpoint that fetches arbitrary URLs can become an SSRF or proxy-abuse path.
  • Enforce a destination policy at the backend and at the network layer. Check redirects too; validating only the initial hostname is insufficient if your policy excludes private destinations.
  • Allowlist render options and impose sensible dimension, selector-length, request-size, and rate limits. Do not pass an arbitrary client object through to URLbox.
  • Keep secrets out of logs, error responses, extension bundles, and URLs visible to users. Return a generic provider error to the extension and retain diagnostic detail in protected server logs.
  • Use HTTPS between extension and backend, and configure cross-origin access only for your extension and application needs.
  • Check that the active tab has an HTTP(S) URL. Browser-internal pages and some protected pages cannot be captured through this flow.

8. Troubleshooting

Symptom Likely cause Fix
URLbox returns an authentication error Missing, incorrect, revoked, or misconfigured project secret; malformed Bearer header. Check the backend secret configuration and send Authorization: Bearer …. Never “fix” this by moving the key into the extension.
Extension fetch fails before a backend response Backend host permission, HTTPS, CORS, or network configuration is wrong. Match the backend origin in extension configuration, serve it over HTTPS, and configure the backend’s access policy for the extension.
Screenshot is blank or incomplete Page may need more time, content may require scrolling, or the target blocks rendering. Try the documented wait controls where applicable, full-page stitch mode for lazy content, and test the target URL. Avoid assuming the extension’s own browser state is available to URLbox.
Element capture becomes a viewport shot The selector may not match the rendered page. The options docs note that a missing selector falls back to a normal viewport screenshot. Inspect the live page in DevTools, use a stable selector, and verify the element exists after the page loads.
Full-page render is slow Stitch mode scrolls through the page to load content; tall pages require more work. Use native mode if its accuracy is acceptable, set a section limit where suitable, or use skip_scroll only when lazy-loaded content is not needed.
Extension UI cannot display the output The backend may return JSON metadata while the UI expects image bytes, or vice versa; large base64 payloads can also be cumbersome. Make the response contract explicit. Return either a temporary render URL or a stored application URL, or stream bytes through an extension-owned view.
Some tabs do not respond to the content script Chrome restricts scripts on browser-internal and protected pages; the tab may also lack a matching content script. Show results in an extension popup, side panel, or extension page and report unsupported tabs clearly.

9. Performance, reliability, and cost

Render latency depends on the target page, its assets, the capture dimensions, and full-page behavior; the supplied documentation provides no universal latency guarantee. Viewport captures generally avoid the repeated scroll-and-stitch work of long pages. The docs say skip_scroll can shave time when the initial scroll is unnecessary, while native full-page capture trades accuracy for speed.

Use a finite backend timeout and communicate that a capture is still running rather than leaving the extension unresponsive. For asynchronous jobs, retain job state on the backend and make status polling or webhook handling idempotent. Retry transient failures selectively, with a cap and backoff; do not blindly repeat a billable render after an ambiguous timeout without understanding provider behavior. URLbox’s reviewed sources do not establish a price, retry guarantee, or cost per capture, so check the current account pricing and terms before estimating spend. Enforce user and plan quotas in your backend.

Or skip the browser setup

ScreenshotNeo is a website screenshot API and MCP server. From your trusted backend, one GET request can capture a URL; the API also supports PNG, JPEG, WebP, PDF, full-page and selector captures, custom viewport sizes, and other options. See the ScreenshotNeo API documentation.

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
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)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);

Keep the ScreenshotNeo API key on your backend too. ScreenshotNeo removes cookie banners, popups, and chat widgets before the shot; bot checks, blank pages, and failed loads are never billed. Its MCP server lets AI agents take screenshots. The free plan includes 1,000 screenshots a month with no card; paid plans start at $5 for 3,000. Sign up free for ScreenshotNeo.

FAQ

Does URLbox provide a Chrome extension manifest?

The reviewed URLbox documentation describes the API and rendering options, not a Chrome-extension-specific manifest. The manifest and backend pattern here are an implementation example.

Can the extension capture the signed-in state of the current Chrome tab?

This flow sends a URL for server-side rendering. It does not automatically share the user’s browser cookies or authenticated session with URLbox. Do not send session cookies to your backend unless your product has a carefully designed, explicit security model for doing so.

How long does a URLbox render URL remain available?

The quickstart says the JSON API’s returned render URL expires after 30 days. Download the image or use configured cloud storage for longer retention.

Can I capture only one element?

Yes. Pass a CSS selector using selector; find a candidate in Chrome DevTools and verify it exists on the rendered version of the page.

Which full-page mode should I start with?

Start with the default stitch mode when accuracy and lazy-loaded content matter. Try native mode when speed matters and the target site renders correctly with it.