Disclosure: I build RenderScreenshot. Browshot has been around since 2011 and does things we do not, such as non-Chromium browser instances and multi-step automation. This page covers those honestly.
The usual reason to look for a Browshot alternative is the request model. Browshot's complete API is asynchronous: you call screenshot/create, poll screenshot/info until the status is finished or error, then download the image or a thumbnail. The simple endpoint hides that loop behind HTTP redirects, but your client still waits through them. RenderScreenshot answers a capture in the same HTTP response, and moves the long-running cases to batch jobs and webhooks. This page shows how each part of a Browshot integration maps across: parameters, instances, the poll loop, multi-URL requests and hosting. It is a spoke of our best screenshot API comparison.
Last verified: October 15, 2026, against the Browshot API documentation. Browshot's pricing page returned HTTP 403 to our checks, so this page quotes no Browshot prices.
Should you switch?
| Stay with Browshot if | Move to RenderScreenshot if |
|---|---|
| You need screenshots from non-Chromium browsers or specific device instances | Chromium output is what you need, with mobile emulation when required |
You script multi-step sessions (steps) or extract data with trackers |
You want one request, one image, no poll loop |
| You send POST data or a referrer on the first request | You want signed, expiring URLs for public <img> tags |
| You prefer pay-as-you-go credits with no subscription | You want presets for social cards and a monthly plan |
| You want wait conditions (network idle, selector) instead of a fixed 5 s delay |
From create-and-poll to one call
A typical Browshot integration has three moving parts. Here is where each one goes:
| Browshot | What it does | RenderScreenshot |
|---|---|---|
GET /api/v1/screenshot/create |
Queues a capture, returns {"status":"in_process","id":...} or a cached result |
POST /v1/screenshot returns the image in the response |
GET /api/v1/screenshot/info?id= (polled) |
Status: in_queue, processing, finished, error |
Not needed. Errors arrive as HTTP status codes with a typed JSON body |
GET /api/v1/screenshot/thumbnail?id= |
Downloads the image or a resized thumbnail | output.resize in the same request; the image is the response body |
GET /api/v1/simple |
One URL that redirects (302) until the image is ready | GET /v1/screenshot?url=... with a key or a signed URL |
hook= parameter |
POST to your URL when finished or failed | webhook.url with signed payloads |
screenshot/multiple |
Up to 10 URLs × 10 instances in one call | POST /v1/batch with per-URL options |
hosting=s3 / hosting=browshot |
Upload to your S3 bucket or Browshot's CDN | storage (your bucket, set up in the dashboard) or the public X-Cache-URL |
The poll loop disappears, and so does the code around it: retry timers, maximum attempts, and the state you kept per screenshot ID. Keep a timeout on the single request instead. Our wait.timeout can go up to 60 seconds; set your HTTP client a little higher.
Parameter map
| Browshot | RenderScreenshot | Notes |
|---|---|---|
key (query) |
Authorization: Bearer rs_live_... |
Or a signed URL for public use |
url |
url |
|
instance_id |
viewport.* or viewport.device |
See the next section |
size=screen / size=page |
capture.mode: viewport / full_page |
full_page preset adds scroll simulation |
screen_width (1 to 5000), screen_height (1 to 10000) |
viewport.width (1 to 7680), viewport.height (1 to 4320) |
Full-page height goes up to capture.scroll.max_height (default 16,000) |
delay (seconds, default 5) |
wait.delay (milliseconds, default 0) |
Prefer wait.until or wait.for_selector |
max_wait |
wait.timeout (seconds, max 60) |
|
target (CSS selector) |
capture.mode: element + capture.selector |
|
hide_popups |
block.cookie_banners, block.ads |
|
dark |
browser.dark_mode |
|
cache (seconds, default 24 h, 0 = fresh) |
cache.ttl (3600 to 2,592,000, default 86,400), cache.refresh |
Our minimum TTL is 1 hour |
headers |
network.headers |
|
cookie (paid instances) |
network.cookies |
Array of cookie objects |
script (URL), script_inline |
page.scripts (string or {url}) |
|
referer, post_data |
network.headers for a Referer; no POST navigation |
Keep Browshot if you need POST data |
strict_ssl |
not available | Certificate failures return the ssl_error code |
html=1 |
output.format: html, or metadata.include |
|
hook |
webhook.url, webhook.events |
|
steps, trackers |
not available | Multi-step automation is out of scope for us |
thumbnail width, height, ratio=fit/fill |
output.resize.width, .height, .fit (contain, cover, ...) |
fit ≈ contain, fill ≈ cover |
thumbnail format (png, jpeg) |
output.format (png, jpeg, webp, avif, pdf) |
Instances become devices
Browshot's model starts from an instance: a browser with a screen size and user agent, numbered (instance 12 is the free one; the docs use 65 as an example premium instance). We start from the page and let you describe the device per request:
- Desktop instances: set
viewport.widthandviewport.height, or use thedesktop_hd(1920x1080) preset. - Mobile instances: use a device preset.
GET /v1/deviceslists them; built-ins includeiphone_14_pro,iphone_14_pro_max,pixel_7,ipad_pro_12andmacbook_pro_14. A device sets viewport, scale, mobile and touch together. - Retina:
viewport.scalefrom 1 to 3.
The trade-off is real: every RenderScreenshot capture is Chromium. If you used Browshot specifically to see a page in another browser engine, read our Chromium vs Firefox vs WebKit comparison and keep a tool that runs those engines.
The five-second default
Browshot waits delay=5 seconds after the page loads, by default. That makes captures reliable on slow pages and adds five seconds to every fast one. When you compare the two services, compare like with like: send delay=0 to Browshot, or wait.delay: 5000 to us. Better, replace the fixed wait on both sides with a condition. For client-rendered apps, wait for a selector that only exists once the data is on the page; our guide to screenshots of single-page applications covers the patterns.
We have not run Browshot through our benchmark, so there are no latency numbers or side-by-side images on this page yet. The Python script below runs both services on the same URL and saves both images, so you can measure on your pages.
Migration code
Node: replace the poll loop
// replace-poll.mjs: the Browshot create -> poll -> download loop becomes one call. // Node 22+, npm i renderscreenshot // Usage: RS_API_KEY=rs_live_... node replace-poll.mjs https://example.com import { Client, TakeOptions, RenderScreenshotError } from 'renderscreenshot'; import { writeFile } from 'node:fs/promises'; const client = new Client(process.env.RS_API_KEY, { timeout: 60_000, ...(process.env.RS_BASE_URL ? { baseUrl: process.env.RS_BASE_URL } : {}), }); // Map the Browshot arguments you already store to TakeOptions. // Browshot waits 5 s after load by default (delay=5). RenderScreenshot waits for // the load event and adds no delay unless you ask, so carry the delay over only // if your pages need it. Better: wait for a selector that proves the page is ready. export function fromBrowshot(b) { let o = TakeOptions.url(b.url); if (b.screen_width) o = o.width(Number(b.screen_width)); if (b.screen_height) o = o.height(Number(b.screen_height)); if (b.size === 'page') o = o.fullPage(); if (b.target) o = o.element(b.target); // CSS selector of one element if (b.hide_popups) o = o.blockCookieBanners().blockAds(); if (b.dark) o = o.darkMode(); if (b.delay !== undefined) o = o.delay(Number(b.delay) * 1000); // seconds -> ms if (b.cache !== undefined && Number(b.cache) >= 3600) o = o.cacheTtl(Number(b.cache)); // RS minimum TTL is 1 hour if (b.mobile) o = o.device('iphone_14_pro'); // instead of a mobile instance_id return o; } try { const t0 = performance.now(); const image = await client.take(fromBrowshot({ url: process.argv[2] ?? 'https://example.com', size: 'page', screen_width: 1280, hide_popups: true, delay: 1 })); await writeFile('page.png', image); console.log(`saved ${image.length} bytes in ${Math.round(performance.now() - t0)} ms`); } catch (err) { if (err instanceof RenderScreenshotError && err.retryable) console.error(`retryable: ${err.code}`); else console.error(err.message); process.exitCode = 1; }
Python: both services, same URL
"""simple_vs_rs.py: Browshot's /api/v1/simple next to the RenderScreenshot call that replaces it. Python 3.10+, standard library only. urllib follows the 302 redirects that Browshot's simple API sends while a screenshot is still in progress. Usage: BROWSHOT_KEY=... RS_API_KEY=rs_live_... python3 simple_vs_rs.py https://example.com """ import json import os import sys import time import urllib.error import urllib.parse import urllib.request BROWSHOT = os.environ.get("BROWSHOT_BASE", "https://api.browshot.com") RS = os.environ.get("RS_BASE_URL", "https://api.renderscreenshot.com") def browshot(url: str) -> bytes: query = urllib.parse.urlencode({ "url": url, "key": os.environ["BROWSHOT_KEY"], "instance_id": os.environ.get("BROWSHOT_INSTANCE", "12"), # 12 is the free instance "size": "page", "screen_width": 1280, "screen_height": 800, "delay": 0, }) with urllib.request.urlopen(f"{BROWSHOT}/api/v1/simple?{query}", timeout=180) as res: return res.read() def renderscreenshot(url: str) -> bytes: body = { "url": url, "viewport": {"width": 1280, "height": 800}, "capture": {"mode": "full_page", "scroll": {"simulate": True}}, # scroll to trigger lazy images "wait": {"until": "networkidle", "timeout": 30}, } req = urllib.request.Request( f"{RS}/v1/screenshot", data=json.dumps(body).encode(), method="POST", headers={"Authorization": f"Bearer {os.environ['RS_API_KEY']}", "Content-Type": "application/json"}, ) with urllib.request.urlopen(req, timeout=90) as res: return res.read() def main(url: str) -> int: status = 0 for name, fn in (("browshot", browshot), ("renderscreenshot", renderscreenshot)): start = time.perf_counter() try: data = fn(url) except urllib.error.HTTPError as err: # Browshot puts the reason in the X-Error header. print(f"{name}: HTTP {err.code} {err.headers.get('X-Error', '')}", file=sys.stderr) status = 1 continue ms = round((time.perf_counter() - start) * 1000) with open(f"{name}.png", "wb") as fh: fh.write(data) print(f"{name}: {len(data)} bytes in {ms} ms") return status if __name__ == "__main__": sys.exit(main(sys.argv[1] if len(sys.argv) > 1 else "https://example.com"))
Browshot answers 404 with a "not found" image when a capture fails, and puts the reason in the X-Error header. urllib raises on the 404, so the script reports it instead of saving the placeholder as if it were a screenshot.
Ruby: screenshot/multiple as one batch
# multiple_to_batch.rb: Browshot's screenshot/multiple (URLs x instances) as one RenderScreenshot batch. # gem install renderscreenshot require 'renderscreenshot' client = RenderScreenshot.client(ENV.fetch('RS_API_KEY'), base_url: ENV['RS_BASE_URL']) urls = ARGV.empty? ? ['https://example.com', 'https://example.org'] : ARGV # One Browshot instance per device becomes one request per device here. devices = { 'desktop' => { width: 1280, height: 800 }, 'mobile' => { device: 'iphone_14_pro' } } requests = urls.product(devices.to_a).map do |url, (_name, opts)| { url: url, options: opts } end result = client.batch_advanced(requests) puts "batch #{result['id']}: #{result['status']}" Array(result['results']).each do |r| puts "#{r['url']} #{r['status']} #{r.dig('image', 'url') || r.dig('error', 'code')}" end
Batches of 10 URLs or fewer return results in the same response; larger batches return a batch ID to check with GET /v1/batch/:id or a webhook. Batch size limits depend on your plan.
All three scripts tested on October 15, 2026 against a local mock that follows Browshot's redirect behaviour and records RenderScreenshot request bodies: Node 22.18.0 with [email protected], Python 3.14.8, Ruby 4.0.7 with the renderscreenshot gem 1.0.0. Not run against either production API.
Rollout
- Find every place that polls. Search for
screenshot/infoandsimple. Each call site becomes one request. - Translate stored options with
fromBrowshot, and decide per call site whether the 5 s delay was doing real work. - Run both for a sample with the Python script, and compare the saved images.
- Move hooks to webhooks. If you used
hook=, pointwebhook.urlat the same handler and verify the signature with the SDK's webhook helper. - Delete the poll code after a clean week.
When Browshot is the better choice
Keep Browshot for jobs that need what we deliberately do not build: screenshots from a specific browser instance other than Chromium, scripted multi-step sessions, trackers that extract prices or ranks while capturing, and POST requests as the first navigation. If those are a small part of your traffic, split it: keep Browshot for those jobs and move plain captures. If they are most of your traffic, Browshot is the right tool and this page should not change your mind.
For everything else, create a free key, translate one stored Browshot request with the Node function above, and compare the result with what Browshot returns today.