Disclosure: I build RenderScreenshot, which competes with both services. This page compares Urlbox and ScreenshotAPI.net on their own terms, from their published documentation and pricing pages. I have no API keys for either, so I print no latency or success numbers. The script to produce them is below.
Short answer: ScreenshotAPI.net is the low-cost option: about a third of Urlbox's list price at 10,000 screenshots a month, unlimited team members, storage export to S3, Google Cloud and Azure on every plan, plus bulk and scheduled screenshots. Urlbox is the deeper and more expensive one: more output formats, signed render links you can put in public HTML, much longer request timeouts, and documentation that lists every option with its default. Choose ScreenshotAPI.net for server-side captures of ordinary pages at volume. Choose Urlbox when captures are hard (slow pages, bot protection), when the image URL must be public, or when you need its formats.
This page is part of our comparison hub. For Urlbox against a budget API with a free tier, see Urlbox vs ApiFlash.
Last verified: Urlbox pricing, render-link and API reference pages read on October 14, 2026. ScreenshotAPI.net pricing and "Get Started" documentation read on October 15, 2026.
The decision table
| Choose ScreenshotAPI.net if | Choose Urlbox if |
|---|---|
| Cost per screenshot decides it | Capture quality on difficult sites decides it |
| Every request comes from your server | You embed image URLs in public pages, emails or og:image tags |
| You want storage export to S3, Google Cloud or Azure on the entry plan | You need a request timeout longer than 100 seconds (Urlbox lists up to 300 seconds) |
| You want bulk and scheduled screenshots from the provider | You need SVG, AVIF, HTML or Markdown output |
| Your whole team needs dashboard access (unlimited members on all plans) | You want async renders with webhooks and a status endpoint |
| A 7-day trial with 100 screenshots is enough to decide | You want a written refund promise (Urlbox offers up to $100 back on the first 10,000 screenshots) |
Plans and limits side by side
Figures from each vendor's pricing page, monthly billing, on the dates above.
| ScreenshotAPI.net | Urlbox | |
|---|---|---|
| Trial | 7 days, 100 screenshots | 7 days, no card |
| Entry | Essential $9: 1,000 screenshots, 20 requests/min, 30 s timeout | Lo-Fi $19: 2,000 renders, 30 requests/min, 30 s timeout, third-party sites as thumbnails only |
| Middle | Startup $29: 10,000 screenshots, 40 requests/min, 60 s timeout | Hi-Fi $49: 5,000 renders, 60/min, 90 s. Ultra $99: 15,000 renders, 250/min, 300 s |
| Top self-serve | Business $175: 100,000 screenshots, 80 requests/min, 100 s timeout | Business: $495 base plus $3 per 1,000 renders, 1,000 requests/min, 300 s |
| Unit | One screenshot | One render (can be more than one per capture, see below) |
| Storage export | S3, Google Cloud, Azure on all plans | S3 from Hi-Fi; any S3-compatible from Ultra |
| Struck-through list prices | $20, $35, $200 | None |
Price at three volumes
| Monthly volume | ScreenshotAPI.net | Urlbox |
|---|---|---|
| 1,000 | $9 (Essential) | $19 (Lo-Fi) if you capture your own sites; $49 (Hi-Fi) for third-party sites |
| 10,000 | $29 (Startup) | $99 (Ultra, 15,000) |
| 100,000 | $175 (Business) | $795 on Business ($495 + 100 × $3), if every capture is one render |
The gap is real. At 100,000 simple captures a month, Urlbox's list price is about 4.5 times ScreenshotAPI.net's. The screenshot API pricing comparison puts both on the same grid as eight other services.
What one capture costs on Urlbox
Urlbox counts renders, and its pricing page lists the cases where one capture uses more than one:
- one render for each 30 seconds a request takes to render;
- one render for each 5 MB of output;
- GPU acceleration uses twice the renders;
- requests through a Urlbox-managed proxy count at that proxy's multiplier, from 2× (geo) to 50× (covert);
- a captcha solved with Urlbox's own keys costs 3 renders.
So a 40-second capture of a slow page, or a 6 MB full-page PNG, counts as at least 2 renders. The page does not say how the time and size rules combine for one request; ask Urlbox if your captures are both slow and large. Urlbox does not charge for failed renders.
ScreenshotAPI.net's pricing page sells "screenshots" and lists the question "How do you define a unique screenshot?" in its FAQ, but the answer was not in the page text we saved. If you capture very long pages or videos, ask how they count before you size a plan.
Auth, caching and response shape
These differences change your code more than any parameter name.
Urlbox has two ways in.
- The REST API:
POST https://api.urlbox.com/v1/render/syncwithAuthorization: Bearer <secret>and a JSON body. It answers with JSON, not the image:renderUrl(a temporary link that expires after 30 days),size,renderTimeandqueueTime. Your code makes a second request to download the file. REST calls are never cached or de-duplicated. After 95 seconds, a long render answers with a307redirect that you follow to keep waiting (redirect_afterchanges the threshold). An async endpoint (/v1/render/async) returns arenderIdand a status URL, or calls your webhook. - Render links:
https://api.urlbox.com/v1/<api-key>/<token>/<format>?<options>. The token is an HMAC-SHA256 of the exact query string, signed with your project secret, so the secret never appears in the URL. The response is the image itself, cached for 30 days by default (ttllowers it,force=truebypasses it). A project setting can reject unsigned links. URLs are limited to 2,048 characters, so largehtml,cssorjsoptions belong in the REST API.
Errors on both paths are JSON: { "error": { "message", "code" }, "requestId" }, with codes such as InvalidURLError, RenderTimeoutError and RateLimitExceededError.
ScreenshotAPI.net has one way in. A request to https://shot.screenshotapi.net/screenshot with your API key in the token query parameter, the target in url, output=image (the file) or output=json, and file_type (the docs list JPEG, PNG, WebP and PDF; their examples use png). The docs say both GET and POST work. Their newer examples use a /v3/screenshot path. Screenshots are served from cache unless you send fresh=true. One of their examples reads the image URL from a Location header, so expect a redirect and let your HTTP client follow it.
Because the token sits in the query string, any ScreenshotAPI.net request URL you put in a web page exposes your key. The documentation we read describes no signing scheme. Keep calls on the server, and serve the stored image from your own storage or CDN.
A Urlbox render link, signed on the server
// urlbox-link.mjs: build an authenticated Urlbox render link for an <img> or og:image tag. // The token is an HMAC-SHA256 of the exact query string, signed with the project secret. import { createHmac } from 'node:crypto'; export function urlboxRenderLink(options, { apiKey, secret, format = 'png', base = 'https://api.urlbox.com' }) { const query = new URLSearchParams(options).toString(); // sign exactly what you send const token = createHmac('sha256', secret).update(query).digest('hex'); return `${base}/v1/${apiKey}/${token}/${format}?${query}`; } if (import.meta.url === `file://${process.argv[1]}`) { const link = urlboxRenderLink( { url: 'https://example.com/', width: '1200', height: '630' }, { apiKey: process.env.URLBOX_API_KEY, secret: process.env.URLBOX_SECRET, base: process.env.URLBOX_BASE }, ); const res = await fetch(link); console.log(res.status, res.headers.get('content-type')); if (!res.ok) process.exitCode = 1; }
The most common bug here is signing one string and sending another: if you build the query twice, or a framework re-encodes it, the token no longer matches and Urlbox answers 401. Build the query once and use the same string for both.
Parameter mapping
Urlbox documents every option with its type and default. For ScreenshotAPI.net, the "Get Started" page we read shows only the basic request; the other features are documented on separate topic pages (viewport and full page, file type and quality, lazy loading and delay, element screenshots, CSS and JavaScript injection, emulation, block resources, PDF rendering, scrolling, bulk and scheduled screenshots, proxy, storage). Where we did not read the parameter name ourselves, the table says so. Check those names in their query builder before you rely on them.
| Purpose | ScreenshotAPI.net | Urlbox |
|---|---|---|
| Auth | token query parameter |
Authorization: Bearer <secret> (REST) or signed render link |
| Target | url |
url or html |
| File format | file_type (PNG, JPEG, WebP, PDF) |
format (png, jpeg, webp, avif, svg, pdf, html, mp4, webm, md) |
| Bytes or JSON | output=image or output=json |
REST: always JSON; render links: bytes, or response_type=json |
| Bypass cache | fresh=true |
force=true (render links) |
| Cache lifetime | not verified | ttl in seconds, default and maximum 30 days |
| Viewport | documented under "Viewport & Full Page", name not verified | width (default 1280), height (default 1024) |
| Full page | documented, name not verified | full_page |
| Element | documented, name not verified | selector |
| Wait | "Lazy Loading & Delay" topic, names not verified | wait_until (domloaded, mostrequestsfinished, requestsfinished, loaded), wait_for, delay (ms) |
| Ads and cookie banners | listed on all plans, names not verified | block_ads, hide_cookie_banners, click_accept |
| Custom CSS/JS | documented, names not verified | css, js |
| Retina / dark mode | "4K & 5K Retina" listed; names not verified | retina, dark_mode |
Head-to-head: how to measure it yourself
No measured numbers on this page. Both services need an account, and we did not run paid captures for this comparison. Here is the test we would run, and the scripts to run it.
- Pick 10 to 20 URLs you will capture in production: some fast static pages, some JavaScript-heavy pages, some with cookie banners, one very long page.
- Use the same viewport (1280×800) and format (PNG) on both.
- Run each URL 3 times per service, one service after the other, from the same machine.
- Record time to the final image (for Urlbox REST, include the download of
renderUrl), bytes, and failures. - Open the first image of each pair and mark it complete, partial (blank areas, missing lazy images, banner visible) or failed. That column is usually where the choice gets made.
// compare.mjs: capture the same URLs on Urlbox and ScreenshotAPI.net, N runs each, // and write one CSV row per attempt. Node 22+, no dependencies. // URLBOX_SECRET=... SCREENSHOTAPI_TOKEN=... node compare.mjs urls.txt 3 > results.csv import { readFile, mkdir, writeFile } from 'node:fs/promises'; const URLBOX = process.env.URLBOX_BASE ?? 'https://api.urlbox.com'; const SSAPI = process.env.SCREENSHOTAPI_BASE ?? 'https://shot.screenshotapi.net'; const VIEW = { width: 1280, height: 800 }; const TIMEOUT = AbortSignal.timeout.bind(AbortSignal, 90_000); const providers = { // Urlbox REST: POST options, get JSON with a renderUrl, then download the file (two hops). async urlbox(url) { 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, format: 'png', ...VIEW }), signal: TIMEOUT(), }); if (!res.ok) throw new Error(`urlbox ${res.status} ${(await res.text()).slice(0, 120)}`); const { renderUrl } = await res.json(); const img = await fetch(renderUrl, { signal: TIMEOUT() }); if (!img.ok) throw new Error(`urlbox download ${img.status}`); return Buffer.from(await img.arrayBuffer()); }, // ScreenshotAPI.net: one GET with the token in the query string; image bytes come back // (fetch follows a redirect if the service answers with one). async screenshotapi(url) { const q = new URLSearchParams({ token: process.env.SCREENSHOTAPI_TOKEN, url, output: 'image', file_type: 'png' }); const res = await fetch(`${SSAPI}/screenshot?${q}`, { redirect: 'follow', signal: TIMEOUT() }); if (!res.ok) throw new Error(`screenshotapi ${res.status} ${(await res.text()).slice(0, 120)}`); return Buffer.from(await res.arrayBuffer()); }, }; const isPng = (b) => b.length > 8 && b.readUInt32BE(0) === 0x89504e47; const [file = 'urls.txt', runsArg = '3'] = process.argv.slice(2); const urls = (await readFile(file, 'utf8')).split('\n').map((l) => l.trim()).filter((l) => l && !l.startsWith('#')); await mkdir('renders', { recursive: true }); console.log('provider,url,run,ms,bytes,ok,error'); let failures = 0; for (const [i, url] of urls.entries()) { for (let run = 1; run <= Number(runsArg); run++) { for (const [name, call] of Object.entries(providers)) { const t0 = performance.now(); try { const buf = await call(url); const ms = Math.round(performance.now() - t0); if (!isPng(buf)) throw new Error('response is not a PNG'); if (run === 1) await writeFile(`renders/${i + 1}-${name}.png`, buf); // keep one image per URL for review console.log(`${name},${url},${run},${ms},${buf.length},1,`); } catch (err) { failures++; console.log(`${name},${url},${run},${Math.round(performance.now() - t0)},0,0,"${String(err.message).replaceAll('"', "'")}"`); } } } } process.exitCode = failures ? 1 : 0;
Then summarize the CSV:
// summarize.mjs: p50, p95, success rate and median bytes per provider from compare.mjs output. // node summarize.mjs results.csv import { readFile } from 'node:fs/promises'; const rows = (await readFile(process.argv[2] ?? 'results.csv', 'utf8')).trim().split('\n').slice(1) .map((l) => l.split(',')).map(([provider, , , ms, bytes, ok]) => ({ provider, ms: +ms, bytes: +bytes, ok: ok === '1' })); const pct = (xs, p) => xs.length ? xs[Math.min(xs.length - 1, Math.ceil((p / 100) * xs.length) - 1)] : NaN; for (const provider of new Set(rows.map((r) => r.provider))) { const mine = rows.filter((r) => r.provider === provider); const good = mine.filter((r) => r.ok); const ms = good.map((r) => r.ms).sort((a, b) => a - b); const bytes = good.map((r) => r.bytes).sort((a, b) => a - b); console.log(`${provider}: ${good.length}/${mine.length} ok, p50 ${pct(ms, 50)} ms, p95 ${pct(ms, 95)} ms, median ${pct(bytes, 50)} bytes`); }
The same test in Python, if that is your stack:
# compare.py: the same test in Python. pip install httpx==0.28.1 # URLBOX_SECRET=... SCREENSHOTAPI_TOKEN=... python compare.py urls.txt 3 > results.csv import csv import os import sys import time import httpx URLBOX = os.environ.get('URLBOX_BASE', 'https://api.urlbox.com') SSAPI = os.environ.get('SCREENSHOTAPI_BASE', 'https://shot.screenshotapi.net') VIEW = {'width': 1280, 'height': 800} def urlbox(client, url): res = client.post(f'{URLBOX}/v1/render/sync', headers={'Authorization': f"Bearer {os.environ['URLBOX_SECRET']}"}, json={'url': url, 'format': 'png', **VIEW}) res.raise_for_status() img = client.get(res.json()['renderUrl']) img.raise_for_status() return img.content def screenshotapi(client, url): params = {'token': os.environ['SCREENSHOTAPI_TOKEN'], 'url': url, 'output': 'image', 'file_type': 'png'} res = client.get(f'{SSAPI}/screenshot', params=params) res.raise_for_status() return res.content def main(path, runs): urls = [l.strip() for l in open(path) if l.strip() and not l.startswith('#')] out = csv.writer(sys.stdout) out.writerow(['provider', 'url', 'run', 'ms', 'bytes', 'ok', 'error']) failures = 0 with httpx.Client(timeout=90, follow_redirects=True) as client: for url in urls: for run in range(1, runs + 1): for name, call in (('urlbox', urlbox), ('screenshotapi', screenshotapi)): t0 = time.perf_counter() try: body = call(client, url) if not body.startswith(b'\x89PNG'): raise ValueError('response is not a PNG') out.writerow([name, url, run, round((time.perf_counter() - t0) * 1000), len(body), 1, '']) except Exception as exc: # record the failure and keep going failures += 1 out.writerow([name, url, run, round((time.perf_counter() - t0) * 1000), 0, 0, str(exc)[:120]]) return 1 if failures else 0 if __name__ == '__main__': sys.exit(main(sys.argv[1] if len(sys.argv) > 1 else 'urls.txt', int(sys.argv[2]) if len(sys.argv) > 2 else 3))
All four scripts were run on October 15, 2026 (Node.js 22.18.0; Python 3.14.8 with httpx 0.28.1) against a local mock of both APIs. The mock checks the paths, the auth, the Urlbox render-link token and the parameter names. With a wrong Urlbox secret, the failure lands in the CSV and the exit code is 1. The scripts have not been run against the live services.
Two notes on reading your results. Time the Urlbox REST path to the downloaded file, not to the JSON, or it looks faster than it is. And run the test twice: ScreenshotAPI.net serves repeat requests from cache unless you add fresh=true, so a second run measures its cache, not its renderer. If you want cold numbers, add fresh: 'true' to its query and force: true for Urlbox render links.
When each one is the better choice
ScreenshotAPI.net fits when you capture many ordinary pages from a backend job: product pages, thumbnails for a directory, archive snapshots. The price per screenshot is low at every tier, storage export is included from the first plan, and bulk and scheduled captures save you from writing a queue. Check the timeout (30 seconds on Essential) against your slowest page.
Urlbox fits when the capture is the hard part: sites behind bot protection (its Ultra tier is sold for this), slow single-page apps that need long timeouts, or teams that need formats beyond images and PDF. It also fits public embeds, because render links are signed and cached. Budget for render multipliers if your pages are slow or large.
Neither fits if you need a free tier for ongoing low volume. Both offer a 7-day trial only.
Where RenderScreenshot fits
If you are choosing between these two because you want signed public image URLs at a price closer to ScreenshotAPI.net's, RenderScreenshot is a third option: signed URLs for public HTML, an X-Cache-URL header with a public CDN link on every image response, presets such as og_card, and a free monthly plan with no card. It has no video output and no scheduled captures. A capture looks like this:
curl -sS -X POST https://api.renderscreenshot.com/v1/screenshot \ -H "Authorization: Bearer $RS_API_KEY" \ -H "Content-Type: application/json" \ -d '{"url":"https://example.com/","viewport":{"width":1280,"height":800},"output":{"format":"png"}}' \ --fail-with-body -o rs.png
Add it to compare.mjs as a third provider to test all three on your URLs. Parameters are in the capture options reference, and the signing scheme is in signed URLs. Get a free API key to run it.
Related
- Urlbox alternative: leaving Urlbox, with a full option map and migration code.
- ScreenshotAPI.net alternative: leaving ScreenshotAPI.net.
- Microlink vs Urlbox: Urlbox against an API that also returns link metadata.
- Screenshot API pricing comparison: ten services at three volumes.