Disclosure: RenderScreenshot is our product. ScreenshotAPI.net is a competitor. ScreenshotAPI.net facts on this page come from its documentation and pricing page, checked on 2026-10-15.
Most ScreenshotAPI.net integrations are a single GET URL: https://shot.screenshotapi.net/v3/screenshot?token=...&url=... plus options, often built by string concatenation in several places across a codebase. The fastest migration path is not to rewrite each call by hand. It is to convert those URLs mechanically, log anything that does not map, and compare the output before you switch.
This page gives you that converter in Node.js, Python and Ruby, the full parameter map behind it, and the three unit and default traps that cause most migration bugs. The screenshot API comparison covers the rest of the market.
TL;DR
| Stay with ScreenshotAPI.net if | Move to RenderScreenshot if |
|---|---|
| Lowest list price at your volume is the deciding factor | You want signed URLs for public <img> and og:image use, so no token sits in your HTML |
| You need scrolling videos (MP4, WebM, GIF) or scheduled screenshots | You want nested JSON options, presets such as og_card, and Markdown input |
| You use its text and HTML extraction | You want element capture that fails fast when the selector is missing (capture.selector_required) |
| You submit very large bulk jobs (its Business plan lists no bulk URL limit) | You want a public CDN link (X-Cache-URL) returned with every render |
Three traps to know before you convert
- Default output is JSON there, bytes here. ScreenshotAPI.net's docs list
outputwith a default of JSON. Our API returns the image unless you sendAccept: application/json. A URL with nooutputparameter therefore means "JSON" and must get theAcceptheader after migration. timeoutunits differ. ScreenshotAPI.net'stimeoutis in milliseconds. Ourwait.timeoutis in seconds, with a maximum of 60.timeout=45000becomeswait.timeout: 45.- The token moves out of the URL. Server-side calls send
Authorization: Bearer rs_live_.... Public embeds use signed URLs, which carry a public key ID and a signature, never the secret.
delay (milliseconds) and ttl (seconds) use the same units in both APIs.
Parameter mapping
Our names are from the POST endpoint reference and the parameter pages it links to.
| ScreenshotAPI.net | RenderScreenshot | Notes |
|---|---|---|
token |
Authorization: Bearer header |
Or a signed URL for public use |
url |
url |
|
custom_html |
html |
Also markdown with markdown_theme |
width, height |
viewport.width, viewport.height |
|
retina=true |
viewport.scale: 2 |
Scale goes up to 3 |
full_page=true |
capture.mode: "full_page" |
|
lazy_load=true |
capture.scroll.simulate: true |
|
scroll_delay (ms) |
capture.scroll.delay (ms) |
|
selector |
capture.mode: "element", capture.selector |
Add capture.selector_required to fail when it is missing |
clip[x], clip[y], ... |
capture.clip {x, y, width, height} |
|
file_type (png, jpg, webp, pdf) |
output.format |
jpg becomes jpeg; we add avif |
image_quality |
output.quality |
|
omit_background=true |
output.transparent: true |
|
thumbnail_width |
output.resize.width |
Resized in the same request |
wait_for_event |
wait.until |
load, domcontentloaded, networkidle |
delay (ms) |
wait.delay (ms) |
|
timeout (ms) |
wait.timeout (s, max 60) |
Convert units |
fresh=true |
cache.refresh: true |
|
enable_caching |
cache.enabled |
Our cache is on by default |
ttl (s) |
cache.ttl (s, 3,600 to 2,592,000) |
|
block_ads |
block.ads |
|
block_tracking |
block.trackers |
|
no_cookie_banners |
block.cookie_banners |
|
block_chat_widgets |
block.chat_widgets |
|
block_js, block_images, block_font, ... |
block.resources: ["script", "image", "font", ...] |
|
block_specific_requests |
block.requests (glob patterns) |
|
css |
page.styles: ["..."] |
|
remove_selector |
page.remove |
|
blur_selector |
page.styles with filter: blur() |
No dedicated option |
selector_to_click |
page.click |
|
cookies |
network.cookies |
Array of cookie objects |
timezone |
browser.timezone |
|
latitude, longitude |
browser.geolocation |
|
output=json |
Accept: application/json |
The converters below cover the common subset. Anything they do not recognize goes into an unmapped list instead of being dropped, so you see every gap on the first run.
The converter
Node.js (Node 22+, no dependencies)
// Convert an existing ScreenshotAPI.net request URL into a RenderScreenshot POST body. // Unknown parameters are returned in `unmapped` so nothing is dropped silently. const truthy = (v) => v === 'true' || v === '1'; export function convert(sapiUrl) { const q = new URL(sapiUrl).searchParams; const body = {}, unmapped = []; const set = (path, value) => { const keys = path.split('.'); let o = body; for (const k of keys.slice(0, -1)) o = o[k] ??= {}; o[keys.at(-1)] = value; }; for (const [k, v] of q) { switch (k) { case 'token': break; // auth moves to the Authorization header case 'url': set('url', v); break; case 'custom_html': set('html', v); break; case 'width': set('viewport.width', Number(v)); break; case 'height': set('viewport.height', Number(v)); break; case 'retina': if (truthy(v)) set('viewport.scale', 2); break; case 'full_page': if (truthy(v)) set('capture.mode', 'full_page'); break; case 'lazy_load': if (truthy(v)) set('capture.scroll.simulate', true); break; case 'scroll_delay': set('capture.scroll.delay', Number(v)); break; case 'selector': set('capture.mode', 'element'); set('capture.selector', v); break; case 'file_type': set('output.format', v === 'jpg' ? 'jpeg' : v); break; case 'image_quality': set('output.quality', Number(v)); break; case 'omit_background': if (truthy(v)) set('output.transparent', true); break; case 'thumbnail_width': set('output.resize.width', Number(v)); break; case 'wait_for_event': set('wait.until', v); break; // load | domcontentloaded | networkidle case 'delay': set('wait.delay', Math.min(Number(v), 10_000)); break; // both in ms case 'timeout': set('wait.timeout', Math.min(Math.ceil(Number(v) / 1000), 60)); break; // ms -> s case 'fresh': if (truthy(v)) set('cache.refresh', true); break; case 'enable_caching': set('cache.enabled', truthy(v)); break; case 'ttl': set('cache.ttl', Math.min(Math.max(Number(v), 3600), 2_592_000)); break; // both in s case 'block_ads': set('block.ads', truthy(v)); break; case 'block_tracking': set('block.trackers', truthy(v)); break; case 'no_cookie_banners': set('block.cookie_banners', truthy(v)); break; case 'block_chat_widgets': set('block.chat_widgets', truthy(v)); break; case 'css': (body.page ??= {}).styles = [...(body.page?.styles ?? []), v]; break; case 'remove_selector': set('page.remove', v.split(',').map((s) => s.trim())); break; case 'blur_selector': // no direct option: blur with injected CSS (body.page ??= {}).styles = [...(body.page?.styles ?? []), `${v} { filter: blur(8px) !important; }`]; break; case 'timezone': set('browser.timezone', v); break; case 'output': break; // handled by the caller: image bytes or JSON default: unmapped.push(k); } } return { body, wantsJson: (q.get('output') ?? 'json') === 'json', unmapped }; } export async function render(sapiUrl) { const { body, wantsJson, unmapped } = convert(sapiUrl); if (unmapped.length) console.warn('not mapped:', unmapped.join(', ')); const res = await fetch('https://api.renderscreenshot.com/v1/screenshot', { method: 'POST', headers: { Authorization: `Bearer ${process.env.RS_API_KEY}`, 'Content-Type': 'application/json', ...(wantsJson ? { Accept: 'application/json' } : {}), }, body: JSON.stringify(body), signal: AbortSignal.timeout(90_000), }); if (!res.ok) throw new Error(`RenderScreenshot ${res.status}: ${await res.text()}`); return wantsJson ? res.json() : Buffer.from(await res.arrayBuffer()); }
Example: this legacy URL
https://shot.screenshotapi.net/v3/screenshot?token=TOKEN&url=https%3A%2F%2Fexample.com%2F&output=image &file_type=webp&full_page=true&lazy_load=true&wait_for_event=networkidle&delay=1500 &no_cookie_banners=true&block_ads=true&blur_selector=.email&timeout=45000&fresh=true&latitude=1
becomes this body (the converter reports latitude as unmapped, because it needs a longitude to form browser.geolocation):
{ "url": "https://example.com/", "output": { "format": "webp" }, "capture": { "mode": "full_page", "scroll": { "simulate": true } }, "wait": { "until": "networkidle", "delay": 1500, "timeout": 45 }, "block": { "cookie_banners": true, "ads": true }, "page": { "styles": [".email { filter: blur(8px) !important; }"] }, "cache": { "refresh": true } }
Python
"""Convert an existing ScreenshotAPI.net request URL into a RenderScreenshot POST body.""" import os from urllib.parse import parse_qsl, urlsplit import requests def _truthy(v): return v in ("true", "1") def _set(body, path, value): *parents, last = path.split(".") for k in parents: body = body.setdefault(k, {}) body[last] = value def convert(sapi_url: str): body, unmapped, output = {}, [], "json" styles = [] for k, v in parse_qsl(urlsplit(sapi_url).query, keep_blank_values=True): if k == "token": continue elif k == "url": _set(body, "url", v) elif k == "custom_html": _set(body, "html", v) elif k == "width": _set(body, "viewport.width", int(v)) elif k == "height": _set(body, "viewport.height", int(v)) elif k == "retina" and _truthy(v): _set(body, "viewport.scale", 2) elif k == "full_page" and _truthy(v): _set(body, "capture.mode", "full_page") elif k == "lazy_load" and _truthy(v): _set(body, "capture.scroll.simulate", True) elif k == "scroll_delay": _set(body, "capture.scroll.delay", int(v)) elif k == "selector": _set(body, "capture.mode", "element"); _set(body, "capture.selector", v) elif k == "file_type": _set(body, "output.format", "jpeg" if v == "jpg" else v) elif k == "image_quality": _set(body, "output.quality", int(v)) elif k == "omit_background" and _truthy(v): _set(body, "output.transparent", True) elif k == "thumbnail_width": _set(body, "output.resize.width", int(v)) elif k == "wait_for_event": _set(body, "wait.until", v) elif k == "delay": _set(body, "wait.delay", min(int(v), 10_000)) elif k == "timeout": _set(body, "wait.timeout", min(-(-int(v) // 1000), 60)) # ms -> s elif k == "fresh" and _truthy(v): _set(body, "cache.refresh", True) elif k == "enable_caching": _set(body, "cache.enabled", _truthy(v)) elif k == "ttl": _set(body, "cache.ttl", min(max(int(v), 3600), 2_592_000)) elif k == "block_ads": _set(body, "block.ads", _truthy(v)) elif k == "block_tracking": _set(body, "block.trackers", _truthy(v)) elif k == "no_cookie_banners": _set(body, "block.cookie_banners", _truthy(v)) elif k == "block_chat_widgets": _set(body, "block.chat_widgets", _truthy(v)) elif k == "css": styles.append(v) elif k == "remove_selector": _set(body, "page.remove", [s.strip() for s in v.split(",")]) elif k == "blur_selector": styles.append(f"{v} {{ filter: blur(8px) !important; }}") elif k == "timezone": _set(body, "browser.timezone", v) elif k == "output": output = v elif k not in ("retina", "full_page", "lazy_load", "omit_background", "fresh"): unmapped.append(k) if styles: _set(body, "page.styles", styles) return body, output == "json", unmapped def render(sapi_url: str): body, wants_json, unmapped = convert(sapi_url) if unmapped: print("not mapped:", ", ".join(unmapped)) headers = {"Authorization": f"Bearer {os.environ['RS_API_KEY']}"} if wants_json: headers["Accept"] = "application/json" r = requests.post("https://api.renderscreenshot.com/v1/screenshot", json=body, headers=headers, timeout=90) r.raise_for_status() return r.json() if wants_json else r.content
The last elif skips boolean flags that were set to false, so they do not show up as unmapped.
Ruby
# Convert an existing ScreenshotAPI.net request URL into a RenderScreenshot POST body. require 'net/http' require 'json' require 'uri' def convert(sapi_url) body = {} unmapped = [] styles = [] output = 'json' set = lambda do |path, value| *parents, last = path.split('.') parents.reduce(body) { |o, k| o[k] ||= {} }[last] = value end truthy = ->(v) { %w[true 1].include?(v) } URI.decode_www_form(URI(sapi_url).query).each do |k, v| case k when 'token' then next when 'url' then set.('url', v) when 'custom_html' then set.('html', v) when 'width' then set.('viewport.width', v.to_i) when 'height' then set.('viewport.height', v.to_i) when 'retina' then set.('viewport.scale', 2) if truthy.(v) when 'full_page' then set.('capture.mode', 'full_page') if truthy.(v) when 'lazy_load' then set.('capture.scroll.simulate', true) if truthy.(v) when 'scroll_delay' then set.('capture.scroll.delay', v.to_i) when 'selector' then set.('capture.mode', 'element'); set.('capture.selector', v) when 'file_type' then set.('output.format', v == 'jpg' ? 'jpeg' : v) when 'image_quality' then set.('output.quality', v.to_i) when 'omit_background' then set.('output.transparent', true) if truthy.(v) when 'thumbnail_width' then set.('output.resize.width', v.to_i) when 'wait_for_event' then set.('wait.until', v) when 'delay' then set.('wait.delay', [v.to_i, 10_000].min) when 'timeout' then set.('wait.timeout', [(v.to_i / 1000.0).ceil, 60].min) # ms -> s when 'fresh' then set.('cache.refresh', true) if truthy.(v) when 'enable_caching' then set.('cache.enabled', truthy.(v)) when 'ttl' then set.('cache.ttl', v.to_i.clamp(3600, 2_592_000)) when 'block_ads' then set.('block.ads', truthy.(v)) when 'block_tracking' then set.('block.trackers', truthy.(v)) when 'no_cookie_banners' then set.('block.cookie_banners', truthy.(v)) when 'block_chat_widgets' then set.('block.chat_widgets', truthy.(v)) when 'css' then styles << v when 'remove_selector' then set.('page.remove', v.split(',').map(&:strip)) when 'blur_selector' then styles << "#{v} { filter: blur(8px) !important; }" when 'timezone' then set.('browser.timezone', v) when 'output' then output = v else unmapped << k end end set.('page.styles', styles) unless styles.empty? [body, output == 'json', unmapped] end def render(sapi_url) body, wants_json, unmapped = convert(sapi_url) warn "not mapped: #{unmapped.join(', ')}" unless unmapped.empty? uri = URI('https://api.renderscreenshot.com/v1/screenshot') headers = { 'Authorization' => "Bearer #{ENV.fetch('RS_API_KEY')}", 'Content-Type' => 'application/json' } headers['Accept'] = 'application/json' if wants_json res = Net::HTTP.start(uri.host, uri.port, use_ssl: true, read_timeout: 90) { |h| h.post(uri.path, body.to_json, headers) } raise "RenderScreenshot #{res.code}: #{res.body}" unless res.is_a?(Net::HTTPSuccess) wants_json ? JSON.parse(res.body) : res.body end
Tested on 2026-10-15 with Node 22.18.0, Python 3.14 (requests 2.34.2) and Ruby 4.0.7. Each converter ran on a sample legacy URL, and the resulting request went to a local mock of our API. We checked the logged JSON against our parameter docs. We did not call either live API.
A safe cutover
- Inventory. Search your code and logs for
shot.screenshotapi.net. Run every distinct URL shape throughconvert()and fix eachunmappedkey. - Shadow mode. For one week, keep serving ScreenshotAPI.net images, but also call
render()in the background for a sample (5 to 10 percent) of requests. Log status, latency and bytes for both. - Look at the images. Pick the 20 pages where the byte sizes differ most and compare them by eye. Size gaps usually mean a cookie banner, a lazy image or a font that one side missed.
- Flip per use case. Move the least risky output first (internal thumbnails), then public ones. Keep the old token valid until the last image URL that points at ScreenshotAPI.net has aged out of your caches.
Price at three volumes
Prices as listed on ScreenshotAPI.net's pricing page and our pricing page, checked on 2026-10-15, monthly billing, USD.
| Screenshots per month | ScreenshotAPI.net | RenderScreenshot |
|---|---|---|
| 1,000 | Essential, $9 (1,000) | Starter, $10 (2,500 credits) |
| 10,000 | Startup, $29 (10,000) | Growth, $40 (12,000 credits) |
| 100,000 | Business, $175 (100,000) | More than Scale's 60,000 credits ($100); contact us |
ScreenshotAPI.net is cheaper on list price at all three points. Its trial gives 100 screenshots over 7 days; our free plan gives 150 credits every month, with no credit card and no end date. Its Essential plan leaves out proxy support, scrolling screenshots and video, as its feature table shows.
When ScreenshotAPI.net is the better choice
- You need video output (scrolling screenshots as MP4, WebM or GIF) or scheduled captures.
- You extract page text or HTML along with the image.
- Cost per screenshot at high volume is your main constraint.
If you need signed URLs for public images, presets for social cards, or nested request options that are easy to review in code, create a free key and run the converter on your top five URL shapes.