Disclosure: I build RenderScreenshot. ApiFlash is a good, cheap API, and this page says when you should keep it.
If you need an ApiFlash alternative, the usual reasons are a public image link that does not expose your access key, presets for social cards, or a cache you can purge. RenderScreenshot covers those. ApiFlash stays cheaper at 1,000 and 100,000 shots a month. This page maps every ApiFlash parameter to ours, prices both, and gives you a drop-in function in Node, Python and Ruby that moves traffic over in steps. It is one of the spokes of our best screenshot API comparison.
Last verified: October 15, 2026, against the ApiFlash documentation and pricing page as published on October 14, 2026.
The short answer
| Choose ApiFlash if | Choose RenderScreenshot if |
|---|---|
| You want the lowest list price at 1,000 or 100,000 shots a month | You put screenshot URLs in public HTML (<img>, og:image) and must not expose a key |
| Every request is server-side, so a key in the query string is fine | You want presets such as og_card (1200×630) instead of building sizes yourself |
You want HTML or text extraction in the same call (extract_html, extract_text) |
You need to delete or purge cached images by key, URL pattern or date |
| You upload to S3 with per-request credentials | You want batch jobs, webhooks, PDF output or AVIF |
| You like JPEG at 1920×1080 as the default | You want an official SDK in Node, Python, Ruby, PHP, Java or Go |
Parameter map
ApiFlash takes everything as flat query or form parameters. RenderScreenshot accepts flat query parameters on GET for the common options and a nested JSON body on POST for everything. The middle column shows the POST name; the GET name is in brackets where one exists.
| ApiFlash | RenderScreenshot | Notes |
|---|---|---|
access_key |
Authorization: Bearer rs_live_... (or api_key, or a signed URL) |
Keys: rs_live_ for API calls, rs_pub_ + rs_secret_ for signing |
url |
url |
Both need the protocol. Encode it in a query string |
width (default 1920) |
viewport.width (width, default 1280) |
Range 1 to 7680 on our side |
height (default 1080) |
viewport.height (height, default 800) |
|
scale_factor (1 or 2) |
viewport.scale (scale, 1 to 3) |
|
full_page |
capture.mode: "full_page" (full_page=true) |
Max height 16,000 px by default (capture.scroll.max_height) |
scroll_page |
capture.scroll.simulate: true |
Triggers lazy-loaded images |
element |
capture.mode: "element" + capture.selector (selector) |
capture.selector_required makes a missing element an error |
element_overlap |
No equivalent | |
crop (left,top,width,height) |
capture.clip: { x, y, width, height } |
|
format (default jpeg) |
output.format (format, default png) |
We add avif, pdf and html |
quality (default 80) |
output.quality (quality, default 80) |
JPEG, WebP, AVIF |
transparent (PNG only) |
output.transparent |
PNG or WebP |
thumbnail_width |
output.resize.width |
Plus resize.height and resize.fit |
delay (seconds, max 10) |
wait.delay (delay, milliseconds) |
Multiply by 1,000 |
wait_for (selector, 15 s) |
wait.for_selector |
wait.for_selector_mode: visible, hidden, attached, detached |
wait_until (dom_loaded, page_loaded, network_idle; default network_idle) |
wait.until (domcontentloaded, load, networkidle; default load) |
Defaults differ: set it explicitly |
wait_until_timeout (max 30 s) |
wait.timeout (timeout, seconds, max 60) |
Ours fails the request on timeout |
css |
page.styles |
Array of strings or { url } |
js |
page.scripts |
Array of strings or { url } |
no_ads |
block.ads (block_ads) |
|
no_tracking |
block.trackers |
|
no_cookie_banners |
block.cookie_banners (block_cookies) |
See the cookie test below |
user_agent |
browser.user_agent |
|
accept_language |
browser.locale or network.headers |
|
headers (a=b;c=d) |
network.headers (JSON object) |
|
cookies (a=b;c=d) |
network.cookies (array of cookie objects) |
Domain, path, secure and SameSite are explicit |
latitude, longitude, accuracy |
browser.geolocation |
|
time_zone |
browser.timezone |
|
proxy |
network.proxy |
|
ip_location (enterprise plans) |
network.country |
|
fail_on_status |
No direct equivalent | Use options.fail_if_contains for error text, or read the status with metadata.include: ["status"] |
ttl (default 1 day, max 30 days) |
cache.ttl (cache_ttl, 1 hour to 30 days, default 1 day) |
|
fresh |
cache.refresh |
|
response_type=json |
Accept: application/json header |
Or read X-Cache-URL from the image response |
extract_html, extract_text |
No same-call equivalent | output.format: "html" is a separate render |
s3_* per request |
storage (credentials stored in the dashboard) |
Path templates such as {year}/{month}/{hash}.{ext} |
Every RenderScreenshot name above is in the parameter reference and blocking options.
Price at three volumes
From each pricing page on October 14, 2026, monthly billing:
| Monthly volume | ApiFlash | RenderScreenshot |
|---|---|---|
| Free | 100 screenshots | 150 credits, no credit card |
| 1,000 | $7 (Lite) | $10 (Starter, 2,500 credits) |
| 10,000 | $35 (Medium) | $40 (Growth, 12,000 credits) |
| 100,000 | $180 (Large) | No self-serve plan; Scale is $100 for 60,000 |
ApiFlash wins on price at both ends. Both services say cache hits do not count: ApiFlash for calls with the same parameters within the TTL, RenderScreenshot for requests sent with cache.available (caching docs). If your traffic repeats, both bills drop. The pricing comparison puts eight more vendors on the same grid.
Head-to-head: what we measured
Nothing yet. Both APIs need a key, and I have not run the paid benchmark at the time of writing, so this page prints no latency or success numbers for either. I will not estimate them.
What I did verify: every request on this page against a local mock that checks the path, auth and parameter names for both services, and the ApiFlash parameter names against the parameter list in its docs. To get your own numbers, run the comparison script from the best screenshot API comparison with both keys:
node compare.mjs --providers apiflash,renderscreenshot --urls urls.txt --runs 3 --out apiflash-vs-rs.csv
Test cookie banners on your own pages
Cookie walls are where screenshot APIs differ most, and lists of known banners age fast. The useful test is your own URLs. This script captures each URL on both services with banner and ad blocking on, and saves the pairs for you to compare by eye:
// cookie-check.mjs: capture each URL on ApiFlash and RenderScreenshot with cookie-banner blocking on, // and save both images side by side for review. Node 22+, no dependencies. // node cookie-check.mjs urls.txt -> out/<n>-apiflash.png, out/<n>-renderscreenshot.png import { mkdir, readFile, writeFile } from 'node:fs/promises'; const W = 1280, H = 800; async function apiflash(url) { const q = new URLSearchParams({ access_key: process.env.APIFLASH_ACCESS_KEY, url, width: W, height: H, format: 'png', no_cookie_banners: 'true', no_ads: 'true', wait_until: 'network_idle', }); return fetch(`https://api.apiflash.com/v1/urltoimage?${q}`, { signal: AbortSignal.timeout(60_000) }); } async function renderscreenshot(url) { return fetch('https://api.renderscreenshot.com/v1/screenshot', { method: 'POST', headers: { Authorization: `Bearer ${process.env.RS_API_KEY}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ url, viewport: { width: W, height: H }, output: { format: 'png' }, block: { cookie_banners: true, ads: true }, wait: { until: 'networkidle' }, }), signal: AbortSignal.timeout(60_000), }); } const urls = (await readFile(process.argv[2] ?? 'urls.txt', 'utf8')).split('\n').map((l) => l.trim()).filter((l) => l && !l.startsWith('#')); await mkdir('out', { recursive: true }); let failures = 0; for (const [i, url] of urls.entries()) { for (const [name, call] of [['apiflash', apiflash], ['renderscreenshot', renderscreenshot]]) { const t0 = performance.now(); const res = await call(url).catch((err) => ({ ok: false, status: 0, text: async () => err.message })); const ms = Math.round(performance.now() - t0); if (!res.ok) { failures++; console.log(`${i + 1} ${name} FAIL ${res.status} ${(await res.text()).slice(0, 100)}`); continue; } const buf = Buffer.from(await res.arrayBuffer()); await writeFile(`out/${i + 1}-${name}.png`, buf); console.log(`${i + 1} ${name} ${ms} ms ${buf.length} bytes ${url}`); } } process.exitCode = failures ? 1 : 0;
Use 10 to 20 pages that show consent banners to visitors from your region: news sites, shops, anything with a consent manager. Look for three outcomes per image: banner gone and page visible, banner gone but page blank behind an overlay, or banner still there. Count them per service. That count is your result for this row of the comparison.
Tested on October 15, 2026 with Node.js 22.18.0 against the local mock (both services, two URLs, exit code 0).
Response and auth differences
- Where the key lives. ApiFlash:
access_keyin the query string or form body, for every call. RenderScreenshot: a Bearer header for server calls. For public HTML, a signed URL carrieskey_id(rs_pub_...),expiresand an HMAC-SHA256signaturemade with yourrs_secret_...key, which never leaves your server. Signed URLs must expire within 30 days. - What comes back. Both return image bytes by default. ApiFlash's JSON mode returns
{ "url": ... }. RenderScreenshot adds headers to the image response:X-Cache-URL(a public CDN link with no key in it),X-Cache-Hit,X-Credits-Used,X-Credits-RemainingandX-Request-Id. - Errors. ApiFlash uses status codes: 400 bad request, 401 bad key, 402 quota exceeded, 403 feature not in plan, 429 rate limit, 500. RenderScreenshot returns a typed JSON error with
type,code,messageand aretryableflag, so your code can decide to retry without parsing text (error codes). - Rate limits. ApiFlash queues requests above 20 per second and rejects them past a burst of 400. RenderScreenshot limits requests per minute and concurrent renders by plan, and sends
Retry-Afteron a 429 (rate limits).
Migration: one function, two providers
Put both providers behind one function and move traffic with an environment variable. The bucket is a hash of the URL, so each URL always goes to the same provider during the rollout and both caches stay warm. Set RS_ROLLOUT_PERCENT=10, watch your error logs for a day, then raise it.
Node.js
// screenshot.mjs: ApiFlash and RenderScreenshot behind one function. // Node 22+, npm i renderscreenshot. Keys come from the environment. import { createHash } from 'node:crypto'; import { writeFile } from 'node:fs/promises'; import { Client, TakeOptions } from 'renderscreenshot'; const rs = new Client(process.env.RS_API_KEY); const ROLLOUT = Number(process.env.RS_ROLLOUT_PERCENT ?? '0'); // 0 to 100 // The same URL always lands in the same bucket, so each provider keeps its cache warm. const bucket = (url) => createHash('sha256').update(url).digest()[0] % 100; async function viaApiFlash({ url, width = 1280, height = 800, fullPage = false, format = 'png', hideCookieBanners = false }) { const q = new URLSearchParams({ access_key: process.env.APIFLASH_ACCESS_KEY, url, width: String(width), height: String(height), format, full_page: String(fullPage), no_cookie_banners: String(hideCookieBanners), }); const res = await fetch(`https://api.apiflash.com/v1/urltoimage?${q}`, { signal: AbortSignal.timeout(60_000) }); if (!res.ok) throw new Error(`ApiFlash ${res.status}: ${await res.text()}`); return Buffer.from(await res.arrayBuffer()); } async function viaRenderScreenshot({ url, width = 1280, height = 800, fullPage = false, format = 'png', hideCookieBanners = false }) { let options = TakeOptions.url(url).width(width).height(height).format(format); if (fullPage) options = options.fullPage(); if (hideCookieBanners) options = options.blockCookieBanners(); return rs.take(options); // Buffer } export async function screenshot(job) { return bucket(job.url) < ROLLOUT ? viaRenderScreenshot(job) : viaApiFlash(job); } if (import.meta.url === `file://${process.argv[1]}`) { const image = await screenshot({ url: process.argv[2] ?? 'https://example.com/', hideCookieBanners: true }); await writeFile('out.png', image); console.log(`out.png: ${image.length} bytes`); }
Python
# screenshot.py: ApiFlash and RenderScreenshot behind one function. # Python 3.9+. pip install httpx git+https://github.com/Render-Screenshot/rs-python.git import hashlib import os import sys import httpx from renderscreenshot import Client, TakeOptions rs = Client(os.environ['RS_API_KEY']) ROLLOUT = int(os.environ.get('RS_ROLLOUT_PERCENT', '0')) def bucket(url): return hashlib.sha256(url.encode()).digest()[0] % 100 def via_apiflash(url, width=1280, height=800, full_page=False, fmt='png'): params = { 'access_key': os.environ['APIFLASH_ACCESS_KEY'], 'url': url, 'width': width, 'height': height, 'format': fmt, 'full_page': str(full_page).lower(), } res = httpx.get('https://api.apiflash.com/v1/urltoimage', params=params, timeout=60) res.raise_for_status() return res.content def via_renderscreenshot(url, width=1280, height=800, full_page=False, fmt='png'): options = TakeOptions.url(url).width(width).height(height).format(fmt) if full_page: options = options.full_page() return rs.take(options) def screenshot(url, **kwargs): provider = via_renderscreenshot if bucket(url) < ROLLOUT else via_apiflash return provider(url, **kwargs) if __name__ == '__main__': image = screenshot(sys.argv[1] if len(sys.argv) > 1 else 'https://example.com/') with open('out.png', 'wb') as f: f.write(image) print(f'out.png: {len(image)} bytes')
Ruby
# screenshot.rb: ApiFlash and RenderScreenshot behind one method. # Ruby 3.0+. gem install renderscreenshot require 'digest' require 'net/http' require 'renderscreenshot' RS = RenderScreenshot.client(ENV.fetch('RS_API_KEY')) ROLLOUT = Integer(ENV.fetch('RS_ROLLOUT_PERCENT', '0')) def bucket(url) Digest::SHA256.digest(url).getbyte(0) % 100 end def via_apiflash(url, width: 1280, height: 800, full_page: false, format: 'png') uri = URI('https://api.apiflash.com/v1/urltoimage') uri.query = URI.encode_www_form( access_key: ENV.fetch('APIFLASH_ACCESS_KEY'), url: url, width: width, height: height, format: format, full_page: full_page ) res = Net::HTTP.get_response(uri) raise "ApiFlash #{res.code}: #{res.body}" unless res.is_a?(Net::HTTPSuccess) res.body end def via_renderscreenshot(url, width: 1280, height: 800, full_page: false, format: 'png') options = RenderScreenshot::TakeOptions.url(url).width(width).height(height).format(format) options = options.full_page if full_page RS.take(options) end def screenshot(url, **kwargs) bucket(url) < ROLLOUT ? via_renderscreenshot(url, **kwargs) : via_apiflash(url, **kwargs) end if $PROGRAM_NAME == __FILE__ image = screenshot(ARGV[0] || 'https://example.com/') File.binwrite('out.png', image) puts "out.png: #{image.bytesize} bytes" end
Tested on October 15, 2026 at RS_ROLLOUT_PERCENT=0 and 100, against the local mock: Node.js 22.18.0 with renderscreenshot 1.1.0, Python 3.14 with the rs-python SDK 1.0.0 and httpx 0.28.1, Ruby 4.0.7 with the renderscreenshot gem 1.0.0. The SDK reference pages: Node, Python, Ruby.
Rollout checklist
- Set
format,widthandheighton both sides. The defaults differ, and a silent switch from JPEG 1920×1080 to PNG 1280×800 changes file sizes and layouts downstream. - Convert every
delayfrom seconds to milliseconds. - Replace
fail_on_statuschecks withoptions.fail_if_containsor a status check, if you rely on them. - Move public
<img>URLs to signed URLs, and regenerate them before they expire. - Raise the rollout in steps: 10, 50, 100. Remove the ApiFlash path a week after 100.
When to stay with ApiFlash
- Price is the deciding factor at 1,000 or 100,000 shots a month, and you do not need public links.
- You use
extract_htmlorextract_textto get the page HTML and text with the same call. - You upload straight to S3 with credentials per request, for example a different bucket per customer. Our storage option uses credentials saved in the dashboard.
- You rely on
element_overlapfor element captures with overlapping content. - It works. If your pages render correctly and nobody needs the key hidden, a migration is cost without benefit.
Try it on your URLs
Create a free RenderScreenshot key, set RS_API_KEY next to your APIFLASH_ACCESS_KEY, and run the cookie test above on ten of your own pages. The free plan covers that test with credits left over.
Related
- Best screenshot API comparison: ten services, one checklist, one test script.
- Screenshot API pricing comparison: the full grid at three volumes.
- ScreenshotOne alternative and Urlbox alternative: if you are also looking at those.
- Blocking parameters: ads, trackers, cookie banners, chat widgets and custom URL patterns.