Disclosure: we build RenderScreenshot, a third screenshot API that competes with both ScreenshotOne and Urlbox. We are not neutral, so this page sticks to facts you can check: each vendor's own documentation and pricing page, read on 2026-10-15 and linked below, plus a test script you run with your own keys. We do not rate either product.
ScreenshotOne and Urlbox are both mature, developer-focused screenshot APIs on managed Chromium. Most comparisons between them argue about features both have. The differences that change a buying decision are fewer: how each one prices volume, how the plans gate features, what the API returns, and what the option names are when you write the integration. This page covers those. Our tool comparisons hub lists other pairings.
Decision table
| If you need | Look first at | Source |
|---|---|---|
| The lowest paid entry price for third-party sites | ScreenshotOne (Basic, $17 for 2,000) | ScreenshotOne pricing |
| Cheap renders of your own pages only (OG images, thumbnails) | Urlbox Lo-Fi ($19 for up to 2,000) | Urlbox pricing |
| Billing only for successful, non-cached renders | ScreenshotOne (states this in its pricing FAQ) | ScreenshotOne pricing FAQ |
| Long render timeouts (up to 300 s) or no file-size cap | Urlbox Ultra or Business | Urlbox plan table |
| SVG or saved HTML output | Urlbox lists both as format values |
Urlbox options |
| Scrolling videos and IP-location choice | ScreenshotOne Growth or higher | ScreenshotOne plan table |
| An uptime SLA on a self-serve plan | Urlbox Business (99.95%) | Urlbox plan table |
| A free tier that does not expire | ScreenshotOne (100 screenshots a month) | ScreenshotOne pricing |
| A trial with paid-plan features | Urlbox (7-day trial, no credit card) | Urlbox pricing |
Price at three volumes
List prices as shown on each pricing page on 2026-10-15, monthly billing, USD, before tax. Urlbox prices plans on a volume slider; where a tier price was not shown on the page, we say so instead of guessing.
| Screenshots per month | ScreenshotOne | Urlbox |
|---|---|---|
| 1,000 | Basic: $17 (2,000 included) | Hi-Fi: $49 (up to 5,000). Lo-Fi: $19 (up to 2,000), own sites and thumbnails only |
| 10,000 | Growth: $79 (10,000 included) | Ultra: $99 (up to 15,000). Hi-Fi above 5,000: price set on the slider |
| 100,000 | Scale $259 (50,000) + 50,000 extra at $0.004 = $459 | Business: $495 base + $3 per 1,000 renders = $795 by its stated formula. Ultra above 15,000: price set on the slider |
Two plan details matter more than the headline price:
- ScreenshotOne gates by feature. Basic includes blocking, PDF, HTML rendering, caching, S3 upload, webhooks and signed links. Growth adds IP-location selection, scrolling screenshots and video. Scale adds GPU rendering and priority support.
- Urlbox gates by target and limits. Lo-Fi is described as screenshots of websites you own, with low-quality third-party thumbnails. Hi-Fi covers "most" third-party sites, Ultra "all" with stealth request handling, and the plans differ in request timeout (30, 90, 300 s), file-size limit (2.5 MB, 10 MB, none) and requests per minute.
For reference, RenderScreenshot lists Starter at $10 (2,500 credits), Growth at $40 (12,000) and Scale at $100 (60,000), with 150 free credits every month; see /pricing.
Option mapping
ScreenshotOne names come from the parameters used across its documentation and examples; Urlbox names come from its options reference. We add our own names in the last column so you can see all three side by side.
| Task | ScreenshotOne | Urlbox | RenderScreenshot |
|---|---|---|---|
| Auth (server side) | access_key query param |
Authorization: Bearer <secret> on /v1/render/sync |
Authorization: Bearer rs_live_... |
| Auth (public embed) | Signed links (signature) |
Render links | Signed URLs |
| Viewport | viewport_width, viewport_height |
width, height |
viewport.width, viewport.height |
| Pixel density | device_scale_factor |
retina (2x) |
viewport.scale (1 to 3) |
| Full page | full_page |
full_page |
capture.mode: "full_page" |
| One element | selector |
selector |
capture.selector |
| Format | format |
format (png, jpeg, webp, avif, svg, pdf, html, mp4, webm, md) |
output.format (png, jpeg, webp, avif, pdf) |
| Quality | image_quality |
quality (JPEG/WebP) |
output.quality |
| Transparent background | omit_background |
transparent |
output.transparent |
| Wait for an event | wait_until |
wait_until (domloaded, loaded, requestsfinished, mostrequestsfinished) |
wait.until (domcontentloaded, load, networkidle) |
| Wait for an element | wait_for_selector |
wait_for |
wait.for_selector |
| Fixed delay | delay |
delay (ms) |
wait.delay (ms) |
| Cookie banners | block_cookie_banners |
hide_cookie_banners, click_accept |
block.cookie_banners |
| Ads, trackers, chats | block_ads, block_trackers, block_chats |
block_ads, block_urls (patterns) |
block.ads, block.trackers, block.chat_widgets |
| Hide elements | hide_selectors |
hide_selector |
page.hide |
| Click before capture | click |
click |
page.click |
| Custom CSS / JS | styles, scripts |
css, js |
page.styles, page.scripts |
| Dark mode | dark_mode |
dark_mode |
browser.dark_mode |
| Print media | media_type |
media |
browser.media |
| User agent | user_agent |
user_agent |
browser.user_agent |
| Headers, cookies | headers, cookies |
header, cookie |
network.headers, network.cookies |
| Cache TTL / fresh render | cache, cache_ttl, cache_key |
ttl (max 2,592,000 s), force |
cache.ttl, cache.refresh |
| Upload to your bucket | store, storage_path |
use_s3 |
storage.enabled, storage.path |
| Webhook | webhook_url |
webhook_url |
webhook.url |
| Fail on missing content | fail_if_content_contains |
fail_if_selector_missing |
options.fail_if_missing, options.fail_if_contains |
Watch the wait semantics when you move between the two. Urlbox's requestsfinished means no more than 0 network connections for at least 500 ms and mostrequestsfinished allows 2, as its options page defines them. Those match Puppeteer's networkidle0 and networkidle2. Check ScreenshotOne's wait_until values in its docs before you map them one to one.
Response shapes
- ScreenshotOne
GET https://api.screenshotone.com/take?access_key=...&url=...returns the rendered file.response_typechanges this (for example to JSON), andstoreuploads to your bucket. - Urlbox
POST https://api.urlbox.com/v1/render/syncreturns JSON withrenderUrlandsize. You download the file fromrenderUrl. The async endpoint and webhooks cover long renders. Render links (/v1/<key>/<token>/<format>?...) return the image for<img>use.
If you build a provider-agnostic wrapper, normalize to "bytes plus a stored URL" early. That is the shape both can produce with one extra step.
Test both on your own URLs
We have not run ScreenshotOne and Urlbox on our shared test corpus yet, so this page has no latency or success-rate table. The script below does the run for you. It sends the same 1280 x 800 full-page PNG request with ads and cookie banners handled to both services, saves every image, and records latency, bytes and pixel dimensions.
Method we recommend:
- Put 10 to 20 URLs in
urls.txt: a static page, a long page with lazy images, a cookie-banner page, a single-page app, a page behind a CDN bot check, and the hardest pages from your own product. - Run 3 times or more per URL. Keep caching off on both sides (
cache=falsefor ScreenshotOne,force: truefor Urlbox) so you measure renders. - Report p50 and p95 latency per provider, and the share of runs that returned an image.
- Open the images side by side. Dimensions that differ for the same URL usually mean one provider stopped scrolling early or a banner changed the layout.
- Repeat on a different day. A single run is a sample, not a benchmark.
Node.js (Node 22+, no dependencies)
// Run the same capture through ScreenshotOne and Urlbox and record latency, bytes and dimensions. // Usage: SCREENSHOTONE_ACCESS_KEY=... URLBOX_SECRET=... node compare.mjs urls.txt import { readFile, writeFile, mkdir } from 'node:fs/promises'; const OPTS = { width: 1280, height: 800, fullPage: true, format: 'png' }; const RUNS = Number(process.env.RUNS ?? 3); const providers = { // GET https://api.screenshotone.com/take returns the image bytes. async screenshotone(url) { const q = new URLSearchParams({ access_key: process.env.SCREENSHOTONE_ACCESS_KEY, url, viewport_width: String(OPTS.width), viewport_height: String(OPTS.height), full_page: String(OPTS.fullPage), format: OPTS.format, block_cookie_banners: 'true', block_ads: 'true', cache: 'false', }); const res = await fetch(`https://api.screenshotone.com/take?${q}`, { signal: AbortSignal.timeout(90_000) }); if (!res.ok) throw new Error(`screenshotone ${res.status}: ${(await res.text()).slice(0, 200)}`); return Buffer.from(await res.arrayBuffer()); }, // POST /v1/render/sync returns JSON with a renderUrl; download it to compare bytes. async urlbox(url) { const res = 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, width: OPTS.width, height: OPTS.height, full_page: OPTS.fullPage, format: OPTS.format, hide_cookie_banners: true, block_ads: true, force: true, }), signal: AbortSignal.timeout(90_000), }); if (!res.ok) throw new Error(`urlbox ${res.status}: ${(await res.text()).slice(0, 200)}`); const { renderUrl } = await res.json(); const img = await fetch(renderUrl, { signal: AbortSignal.timeout(30_000) }); return Buffer.from(await img.arrayBuffer()); }, }; // Width and height from the PNG header (bytes 16-23). const pngSize = (b) => (b.toString('ascii', 1, 4) === 'PNG' && b.length >= 24 ? { w: b.readUInt32BE(16), h: b.readUInt32BE(20) } : null); const urls = (await readFile(process.argv[2] ?? 'urls.txt', 'utf8')).split('\n').map((s) => s.trim()).filter(Boolean); await mkdir('out', { recursive: true }); const rows = []; for (const url of urls) { for (const [name, call] of Object.entries(providers)) { for (let run = 1; run <= RUNS; run++) { const t0 = performance.now(); try { const img = await call(url); const ms = Math.round(performance.now() - t0); const slug = new URL(url).hostname.replace(/\W+/g, '-'); await writeFile(`out/${slug}-${name}-${run}.png`, img); rows.push({ url, provider: name, run, ms, bytes: img.length, ...pngSize(img), ok: true }); } catch (err) { rows.push({ url, provider: name, run, ms: Math.round(performance.now() - t0), ok: false, error: err.message }); } } } } await writeFile('results.json', JSON.stringify(rows, null, 2)); console.table(rows.map(({ error, ...r }) => r));
Urlbox's latency in this script includes the download from renderUrl, because that is what your code waits for. If you only need the stored URL, time the render call alone.
Python
"""Same capture through ScreenshotOne and Urlbox; prints one JSON line per call.""" import json import os import sys import time import requests W, H = 1280, 800 def screenshotone(url): r = requests.get("https://api.screenshotone.com/take", params={ "access_key": os.environ["SCREENSHOTONE_ACCESS_KEY"], "url": url, "viewport_width": W, "viewport_height": H, "full_page": "true", "format": "png", "block_cookie_banners": "true", "block_ads": "true", "cache": "false"}, timeout=90) r.raise_for_status() return r.content def urlbox(url): r = requests.post("https://api.urlbox.com/v1/render/sync", headers={"Authorization": f"Bearer {os.environ['URLBOX_SECRET']}"}, json={"url": url, "width": W, "height": H, "full_page": True, "format": "png", "hide_cookie_banners": True, "block_ads": True, "force": True}, timeout=90) r.raise_for_status() img = requests.get(r.json()["renderUrl"], timeout=30) img.raise_for_status() return img.content for url in [u.strip() for u in open(sys.argv[1]) if u.strip()]: for name, call in (("screenshotone", screenshotone), ("urlbox", urlbox)): t0 = time.perf_counter() try: img = call(url) size = (int.from_bytes(img[16:20], "big"), int.from_bytes(img[20:24], "big")) if img[1:4] == b"PNG" else None print(json.dumps({"url": url, "provider": name, "ms": round((time.perf_counter() - t0) * 1000), "bytes": len(img), "size": size, "ok": True})) except requests.RequestException as e: print(json.dumps({"url": url, "provider": name, "ok": False, "error": str(e)[:200]}))
Tested on 2026-10-15 with Node 22.18.0 and Python 3.14 (requests 2.34.2) against a local mock that imitates both APIs' request and response shapes. The mock confirmed the query string, the JSON body, the renderUrl download and the PNG header parsing. The parameter names come from each vendor's docs; we have not called either live API with these scripts, so check the first results by eye.
Where each one is stronger, from their own pages
ScreenshotOne publishes its plan table with features per tier, bills only successful and non-cached screenshots according to its pricing FAQ, and keeps a free tier of 100 screenshots a month. Its blocking options (block_cookie_banners, block_chats, block_trackers, block_banners_by_heuristics) are many and specific.
Urlbox lists a wide set of format values in one parameter (png, jpeg, webp, avif, svg, pdf, html, mp4, webm, md), separates sync, async and render-link APIs clearly, lists long request timeouts and no file-size cap on upper plans, and states uptime SLAs on Business and Enterprise. Its Lo-Fi plan is a cheap option for the narrow case of rendering your own pages.
Where RenderScreenshot fits
If neither one fits your price point or your stack, we are the third option to test with the same script: add a third provider that posts the same capture to https://api.renderscreenshot.com/v1/screenshot with viewport, capture.mode: "full_page" and block.cookie_banners. The ScreenshotOne alternative and Urlbox alternative pages map each one to our API in detail, and you can run the comparison with a free key.