Disclosure: I build RenderScreenshot, which competes with both services on this page. I have no affiliation with Urlbox or ApiFlash. Every fact about them below comes from their own documentation and pricing pages, checked on October 15, 2026, and the links are next to the claims.
Urlbox and ApiFlash both turn a URL into an image with one HTTP call, and both run Chrome. The decision is not about engine quality. It comes down to three things: what you pay at your volume, whether you need output other than images, and whether your screenshot URLs ever appear in public HTML. ApiFlash wins on price and simplicity. Urlbox wins on output formats, render controls and safe public links.
This page is part of our comparison hub. If you are leaving one of them, the migration guides for Urlbox and ApiFlash map every parameter to ours.
Last verified: October 15, 2026, against Urlbox render options, Urlbox pricing, the ApiFlash documentation and the pricing table on apiflash.com.
The short answer
| Choose ApiFlash if | Choose Urlbox if |
|---|---|
| You want the lowest list price at 1,000, 10,000 or 100,000 shots a month | You need PDF, HTML, Markdown, SVG or video output from the same API |
| Every call is server-side, so a key in the query string is acceptable | You embed screenshot URLs in public pages and need signed render links |
| A free tier of 100 screenshots a month is enough to evaluate | You capture third-party sites that block simple headless browsers (Urlbox sells this as its Ultra tier) |
You want HTML or text extraction in the same call (extract_html, extract_text) |
You need a long request timeout (Urlbox lists up to 300 s on Ultra and Business) |
| You upload results to S3 with per-request credentials | You want click_accept (click the consent button) as well as hiding cookie banners |
If none of the right-hand column applies to you, ApiFlash costs less and you will not miss anything.
Defaults that change your output
Most "Urlbox looks different from ApiFlash" reports are not rendering differences. They are default settings. If you send only a URL to each service, you compare different screenshots:
| Setting | ApiFlash default | Urlbox default | What to send to both |
|---|---|---|---|
| Viewport width | 1920 | 1280 | width=1280 |
| Viewport height | 1080 | 1024 | height=800 |
| Output format | jpeg |
png |
format=png |
| Wait condition | network_idle |
loaded (load event) |
ApiFlash wait_until=page_loaded, Urlbox wait_until=loaded |
| Cache | 24 h (ttl=86400), max 30 days |
30 days (ttl=2592000), also the max |
ApiFlash fresh=true, Urlbox force=true |
| Response body | image bytes | JSON with a renderUrl (sync API) |
read the bytes, or download renderUrl |
The wait condition matters most. ApiFlash's network_idle waits for in-flight requests to finish, so on a page with analytics beacons or a chat widget it takes longer and captures more lazy content. Urlbox's loaded returns sooner and can miss images that load after the load event. Neither is wrong. Pick one condition and set it on both.
The cache matters for benchmarks. Both services return a cached image for an identical request, and a cached response is much faster than a render. ApiFlash says cached responses do not count against your quota. If you test without fresh=true / force=true, your second run measures their CDN, not their browsers.
Pricing at three volumes
List prices, monthly billing, checked on October 15, 2026. Taxes excluded.
| Monthly volume | ApiFlash | Urlbox | Notes |
|---|---|---|---|
| 1,000 | $7 (Lite, 1,000) | $19 (Lo-Fi, 2,000) or $49 (Hi-Fi, 5,000) | Urlbox Lo-Fi captures third-party sites as thumbnails only |
| 10,000 | $35 (Medium, 10,000) | $99 (Ultra, 15,000) | Hi-Fi stops at 5,000 renders |
| 100,000 | $180 (Large, 100,000) | Business: $495 base, then $3 per 1,000 renders | Read Urlbox's Business terms for how many renders the base includes |
| Free option | 100 screenshots/month, no card | 7-day trial, no card |
Sources: the pricing table on apiflash.com and urlbox.com/pricing. Urlbox also lists an Enterprise plan from $3,000 a month, and both services offer custom plans.
Two things the table does not show. First, Urlbox's tiers gate capability, not only volume: third-party sites, "stealth" request privacy, request timeout (30 s, 90 s, 300 s) and file-size limits (2.5 MB, 10 MB, none) all change by plan. A full-page capture of a long marketing page can exceed 2.5 MB as PNG, so check the limit for your plan. Second, ApiFlash's lower price assumes you are fine with its feature set: no PDF, no video, and a 10 s maximum for delay.
Parameter mapping
Parameters that exist on both sides, with their names. Use it to port a request or to build the comparison below.
| Purpose | ApiFlash | Urlbox |
|---|---|---|
| Target | url (must be URL-encoded) |
url or html |
| Viewport | width, height |
width, height |
| Full page | full_page=true |
full_page=true |
| Scroll first (lazy images) | scroll_page=true |
full-page captures scroll and stitch by default (full_page_mode=stitch) |
| One element | element (+ element_overlap) |
selector (+ fail_if_selector_missing) |
| Format | format = jpeg, png, webp |
format = png, jpeg, webp, avif, svg, pdf, html, mp4, webm, md |
| Quality | quality (0 to 100) |
quality (JPEG/WebP) |
| Retina | scale_factor=2 |
retina=true |
| Thumbnail | thumbnail_width |
thumb_width |
| Wait for selector | wait_for (15 s limit) |
wait_for |
| Wait condition | wait_until = domloaded, pageloaded, network_idle |
wait_until = domloaded, mostrequestsfinished, requestsfinished, loaded |
| Fixed delay | delay in seconds (max 10) |
delay in milliseconds |
| Ads | no_ads |
block_ads |
| Cookie banners | no_cookie_banners |
hide_cookie_banners or click_accept |
| Custom CSS / JS | css, js |
css, js |
| Transparent background | transparent (PNG only) |
transparent (PNG/WebP) |
| Dark mode | not documented | dark_mode |
| Cache | ttl, fresh |
ttl, force, unique |
| JSON instead of bytes | response_type=json |
default on the sync API; response_type for render links |
Watch the delay units. delay=3 is three seconds on ApiFlash and three milliseconds on Urlbox.
Auth and response shape
ApiFlash is a single endpoint, GET or POST https://api.apiflash.com/v1/urltoimage. The access_key travels in the query string or the form body. The response is the image, with X-Quota-Limit, X-Quota-Remaining and X-Quota-Reset headers. Errors use plain HTTP codes: 402 when the monthly quota is used up, 403 when your plan lacks a feature you asked for, 429 above 20 requests per second (burst of 400).
Urlbox has a synchronous API (POST https://api.urlbox.com/v1/render/sync with Authorization: Bearer <secret>), an async API with webhooks, and render links. The sync API answers with JSON: a renderUrl on Urlbox's storage plus size and dimensions. Your code needs a second request to fetch the bytes, or it can store the link.
The practical difference: a URL that carries an ApiFlash key must stay on your server. If you put it in an <img src> or an og:image tag, anyone can copy the key and spend your quota. Urlbox render links are signed with your secret, so the secret never appears in the URL. If your screenshots end up in public HTML, that alone can decide the comparison.
The head-to-head test
We have not published measured numbers for this pair yet. Both services have paid tiers that change behaviour, so a fair run needs a paid account on each, the same corpus and the same region. Until we have that, here is the method we use for every pair on this site and the script that runs it. Run it with your own keys on your own URLs. That result is worth more to you than ours.
Method
- Corpus: 10 to 20 of your own URLs: a static page, a page with web fonts, a long page with lazy images, a page with a cookie banner, a single-page app.
- Settings sent to both: 1280x800, PNG, the cache bypassed (
fresh=true,force=true), no delay. - Runs: at least 3 per URL per provider, from one machine, sequentially, so neither service competes with the other for your bandwidth.
- Report: p50 and p95 latency, success rate (a 200 response that is a valid PNG), and output bytes. Then open the renders side by side.
- Count Urlbox's download: the script times the JSON call and the
renderUrlfetch together, because your code waits for both.
The script (Node 22 or later, no dependencies):
// compare.mjs: render the same URLs with ApiFlash and Urlbox, then print latency and bytes. // Node 22+. Usage: APIFLASH_KEY=... URLBOX_SECRET=... node compare.mjs urls.txt import { readFile, writeFile, mkdir } from 'node:fs/promises'; const RUNS = Number(process.env.RUNS ?? 3); const APIFLASH = process.env.APIFLASH_BASE ?? 'https://api.apiflash.com'; const URLBOX = process.env.URLBOX_BASE ?? 'https://api.urlbox.com'; const VIEWPORT = { width: 1280, height: 800 }; const providers = { // ApiFlash: GET, key in the query string, image bytes in the body. async apiflash(target) { const qs = new URLSearchParams({ access_key: process.env.APIFLASH_KEY, url: target, width: String(VIEWPORT.width), height: String(VIEWPORT.height), format: 'png', fresh: 'true', // skip ApiFlash's cache so every run is a real render }); const res = await fetch(`${APIFLASH}/v1/urltoimage?${qs}`, { signal: AbortSignal.timeout(60_000) }); if (!res.ok) throw new Error(`apiflash ${res.status}: ${await res.text()}`); return Buffer.from(await res.arrayBuffer()); }, // Urlbox: POST /v1/render/sync with the secret as a Bearer token. // The default response is JSON with a renderUrl, so the image is a second request. async urlbox(target) { const res = await fetch(`${URLBOX}/v1/render/sync`, { method: 'POST', headers: { Authorization: `Bearer ${process.env.URLBOX_SECRET}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ url: target, width: VIEWPORT.width, height: VIEWPORT.height, format: 'png', force: true }), signal: AbortSignal.timeout(60_000), }); if (!res.ok) throw new Error(`urlbox ${res.status}: ${await res.text()}`); const { renderUrl } = await res.json(); const img = await fetch(renderUrl, { signal: AbortSignal.timeout(60_000) }); if (!img.ok) throw new Error(`urlbox download ${img.status}`); return Buffer.from(await img.arrayBuffer()); }, }; const isPng = (b) => b.length > 8 && b.readUInt32BE(0) === 0x89504e47; const pct = (xs, p) => [...xs].sort((a, b) => a - b)[Math.max(0, Math.ceil((p / 100) * xs.length) - 1)]; const urls = (await readFile(process.argv[2] ?? 'urls.txt', 'utf8')).split('\n').map((s) => s.trim()).filter(Boolean); await mkdir('renders', { recursive: true }); const rows = ['provider,url,runs,ok,p50_ms,p95_ms,bytes']; for (const target of urls) { for (const [name, call] of Object.entries(providers)) { const times = []; let bytes = 0; for (let i = 0; i < RUNS; i++) { const t0 = performance.now(); try { const buf = await call(target); if (!isPng(buf)) throw new Error('response is not a PNG'); times.push(performance.now() - t0); bytes = buf.length; if (i === 0) await writeFile(`renders/${name}-${encodeURIComponent(target)}.png`, buf); } catch (err) { console.error(`${name} ${target}: ${err.message}`); process.exitCode = 1; // keep going, but do not report success } } rows.push([name, target, RUNS, times.length, Math.round(pct(times, 50) ?? 0), Math.round(pct(times, 95) ?? 0), bytes].join(',')); } } await writeFile('results.csv', rows.join('\n') + '\n'); console.log(rows.join('\n'));
Tested on October 15, 2026 with Node 22.18.0 against a local mock of both endpoints (request shape, auth, PNG check, CSV output). It was not run against the live services, because that needs paid keys for both.
Three details in the script are deliberate. It checks the PNG magic bytes, because an error page delivered with HTTP 200 is a failure you would otherwise count as a success. It sets a non-zero exit code on any failure, so a CI job cannot pass by accident. And it saves the first render of each URL, because latency without the picture tells you nothing about quality.
When you read the results, look at the renders before the numbers. A provider that is 300 ms faster because it captured the page before the hero image loaded has not won.
Where each one is the better choice
ApiFlash is the better choice when you run a server-side job (thumbnails for a directory, previews in an admin panel, periodic captures of pages you own), your volume is predictable, and cost is the main constraint. Its parameter set covers the common needs: full page, element, scroll for lazy images, ad and cookie-banner blocking, custom CSS and JS, geolocation and time zone. The one-endpoint design is easy to call from any language without an SDK.
Urlbox is the better choice when the screenshot is part of a product feature that needs more than an image: PDF exports, Markdown or HTML snapshots for an AI pipeline, video of a scroll, or captures of sites that block ordinary headless traffic. Its option list is one of the longest we have read, and the signed render links make public embedding safe.
Neither is the better choice when you need a public, expiring link with a cache you can purge, or you do not want to pay for a capability tier to get it. That is the gap RenderScreenshot fills: signed URLs that expire after at most 30 days, a public X-Cache-URL CDN link on every response, presets such as og_card (1200x630), cache purge by key or URL pattern, and an SDK in six languages. Our full field comparison is in the best screenshot API comparison.
Run the comparison with your own URLs
Put 10 URLs you care about in urls.txt, run the script above with keys for both services, and open the renders folder. If you want a third column, create a free RenderScreenshot key and add one more provider function that calls POST https://api.renderscreenshot.com/v1/screenshot with your key as a Bearer token.