How to Set a URL Dynamically in a JavaScript Screenshot API
Pass changing URLs safely to a hosted screenshot API or Playwright, with encoding, authentication, full-page capture, troubleshooting, and ScreenshotNeo examples.
Direct answer: build the destination as a URL value, then encode it in the screenshot request’s url parameter. With Playwright, pass that value to page.goto(url) and call page.screenshot() afterward. The navigation selects the page; the screenshot call captures the page that is already open.
Choose the right screenshot model
A hosted screenshot API runs the browser for you. Your JavaScript sends a URL over HTTP and receives image bytes. A local browser workflow such as Playwright runs inside your application, so your code navigates directly to the target URL and controls the browser lifecycle.
| Need | Use | Where the URL goes |
|---|---|---|
| Managed rendering over HTTP | Hosted screenshot API | Encoded url query parameter |
| Full browser control | Playwright | page.goto(url) |
Hosted API: construct and encode a dynamic URL
Use URL and URLSearchParams instead of concatenating an unescaped string. This preserves query strings, fragments, spaces, Unicode characters, and ampersands inside the destination URL.
const recordId = '42';
const source = 'home';
const target = new URL('https://example.com/article');
target.searchParams.set('id', recordId);
target.searchParams.set('ref', source);
const endpoint = new URL('https://your-screenshot-provider.example/v1/screenshot');
endpoint.searchParams.set('url', target.href);
const response = await fetch(endpoint, {
headers: { Authorization: `Bearer ${process.env.SCREENSHOT_API_KEY}` }
});
if (!response.ok) {
throw new Error(`Screenshot request failed: ${response.status}`);
}
const imageBytes = Buffer.from(await response.arrayBuffer());
await import('node:fs/promises').then(fs => fs.writeFile('screenshot.png', imageBytes));
The hosted response is image data in the response body, not JSON containing a separate image URL. Check the provider’s documented content type and supported options before adding format or viewport parameters.
When the base URL is supplied separately
function articleUrl(base, id, ref) {
const url = new URL(`/article`, base);
url.search = new URLSearchParams({ id: String(id), ref }).toString();
return url.href;
}
const target = articleUrl('https://example.com', 42, 'email');
Do not leak credentials
Keep production keys in server-side code. A query-string key can appear in browser history, page source, proxy logs, and analytics systems. If your provider supports it, use an Authorization: Bearer ... header. Never put a production key in public browser JavaScript.
Playwright: navigate first, capture second
Install Playwright, start a browser, navigate to the dynamically built URL, and then capture. The fullPage option captures the full scrollable page; omit it for the current viewport.
import { chromium } from 'playwright';
const productId = '42';
const target = new URL('https://example.com/products/view');
target.searchParams.set('id', productId);
a const browser = await chromium.launch();
try {
const page = await browser.newPage({ viewport: { width: 1440, height: 900 } });
await page.goto(target.href, { waitUntil: 'networkidle', timeout: 30_000 });
await page.screenshot({ path: 'product.png', fullPage: true });
} finally {
await browser.close();
}
Replace the accidental a const typo if copying this snippet: the declaration must be const browser = await chromium.launch();.
Capture one area
const card = page.locator('[data-testid="product-card"]');
await card.screenshot({ path: 'card.png' });
// Or use a fixed rectangle:
await page.screenshot({
path: 'clip.png',
clip: { x: 0, y: 0, width: 800, height: 600 }
});
Wait for application content
await page.goto(target.href, { waitUntil: 'domcontentloaded' });
await page.locator('[data-testid="chart"]').waitFor({ state: 'visible', timeout: 15_000 });
await page.screenshot({ path: 'chart.png' });
Use a selector wait for a known readiness signal. A fixed delay is useful only when the page has no reliable selector. Network idle can be unsuitable for pages with analytics, WebSockets, or long-polling requests.
Complete request examples
cURL
curl -G "https://api.screenshotneo.com/v1/shot" \
-d access_key=YOUR_API_KEY \
--data-urlencode "url=https://example.com/article?id=42&ref=home" \
-o shot.webp
Python
import requests
params = {
"access_key": "YOUR_API_KEY",
"url": "https://example.com/article?id=42&ref=home",
}
r = requests.get("https://api.screenshotneo.com/v1/shot", params=params, timeout=90)
r.raise_for_status()
with open("shot.webp", "wb") as f:
f.write(r.content)
Node.js
const target = new URL('https://example.com/article');
target.searchParams.set('id', '42');
target.searchParams.set('ref', 'home');
const q = new URLSearchParams({
access_key: 'YOUR_API_KEY',
url: target.href
});
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`Screenshot failed: ${res.status}`);
const image = Buffer.from(await res.arrayBuffer());
await import('node:fs/promises').then(fs => fs.writeFile('shot.webp', image));
See the ScreenshotNeo API documentation for the available capture parameters and response headers.
URL construction edge cases
- Existing query parameters: use
target.searchParams.set()so existing parameters are retained and values are encoded. - Fragments: fragments are not sent to servers. A browser can use them for client-side routing, but a hosted renderer must support that application route.
- Relative paths: resolve them against a trusted base with
new URL(path, base). - Unicode and spaces: keep them in the URL object; URL encoding handles them at request time.
- Untrusted input: validate allowed protocols and hosts before making a server-side request. At minimum, require
https:(and explicitly decide whetherhttp:is allowed). - Authentication: pass destination cookies or headers only through a provider feature designed for it; do not put secrets in the page URL.
Useful capture controls
| Goal | Playwright approach | Hosted API equivalent |
|---|---|---|
| Entire page | fullPage: true |
Enable the provider’s full-page option |
| Specific element | locator.screenshot() |
Use a CSS selector option |
| Stable dynamic page | Wait for a selector or application state | Use selector, delay, or network-idle waits |
| Remove changing UI | Hide elements with CSS or page scripts | Use custom CSS, JavaScript, or hide selectors |
| Responsive output | Set viewport and device scale factor | Choose a device preset, viewport, and retina scale |
Or skip the browser setup
ScreenshotNeo accepts a URL in one GET request and returns PNG, JPEG, WebP, or PDF output. Cookie and consent banners are accepted and removed before capture, along with more than 60 known consent platforms, newsletter popups, and chat widgets. Bot checks, blank pages, timeouts, failed loads, and cache hits are not billed, and response headers identify the page verdict and whether the request was billed. Its MCP server provides take_screenshot, get_page_info, and capture_pdf tools for Claude, Cursor, and other MCP clients.
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://example.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
There is a free plan with 1,000 screenshots each month and no card. Paid plans start at $5 for 3,000 shots. Create a free ScreenshotNeo account.
Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
Target URL stops at the first & |
Raw URL was concatenated into a query string | Use URLSearchParams, params, or cURL --data-urlencode. |
| 401 or 403 response | Missing, invalid, or exposed credential | Check the key, use the required authentication method, and keep it server-side. |
| Blank screenshot | Capture ran before client rendering completed | Wait for a meaningful selector or application-ready signal. |
| Missing lazy images | Images load only after scrolling | Use full-page capture or explicitly scroll before capture. |
| Timeout | Slow origin, blocked resource, or never-ending network activity | Set a bounded timeout, wait for a selector instead of network idle, and inspect the target directly. |
| Wrong route in a single-page app | Fragment or client routing is not supported by the renderer | Use a server-resolvable URL or configure the renderer for the app’s route. |
| Different pixels on each run | Animations, ads, timestamps, or responsive dimensions vary | Fix viewport and timezone, disable animation with CSS, hide unstable selectors, and wait for stable content. |
Performance, reliability, and cost
- Reuse a Playwright browser process and create isolated pages per job; launching a new browser for every URL adds startup time.
- Bound navigation and capture timeouts, record status codes, and retry only transient failures with backoff.
- Cache identical targets when the page can tolerate stale content. For hosted APIs, choose a cache TTL deliberately.
- Limit concurrency to what your CPU, memory, and origin can handle. Large full-page images consume more memory than viewport captures.
- For repeatable output, fix viewport, device scale, timezone, locale, geolocation, and authentication state.
- With ScreenshotNeo, only clean captures are billed; verdict and billing headers let you reconcile usage and investigate failures.
FAQ
How do I pass a URL to a screenshot API in JavaScript?
Put the absolute destination in the request’s url parameter and let URLSearchParams encode it.
Does page.screenshot() navigate?
No. Call page.goto(url) first; screenshot captures the current page.
Should the API return JSON?
Many screenshot endpoints return binary image bytes directly. Handle the response as an array buffer unless the provider documents a JSON response.
Can I expose the API key in frontend code?
Keep production credentials on a trusted server and proxy requests from your frontend.


