ApiFlash Alternative: Parameter Map and Migration

An ApiFlash alternative with signed links and presets: full parameter map, ApiFlash pricing at three volumes, and tested Node, Python and Ruby migration code.

By Simon Chiu · Updated

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

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_key in the query string or form body, for every call. RenderScreenshot: a Bearer header for server calls. For public HTML, a signed URL carries key_id (rs_pub_...), expires and an HMAC-SHA256 signature made with your rs_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-Remaining and X-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, message and a retryable flag, 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-After on 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

  1. Set format, width and height on both sides. The defaults differ, and a silent switch from JPEG 1920×1080 to PNG 1280×800 changes file sizes and layouts downstream.
  2. Convert every delay from seconds to milliseconds.
  3. Replace fail_on_status checks with options.fail_if_contains or a status check, if you rely on them.
  4. Move public <img> URLs to signed URLs, and regenerate them before they expire.
  5. 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_html or extract_text to 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_overlap for 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.

Frequently asked questions

Is ApiFlash cheaper than RenderScreenshot?
At 1,000 screenshots a month (ApiFlash Lite, $7) and at 100,000 (ApiFlash Large, $180), yes, by list price on October 14, 2026. At 10,000, ApiFlash Medium is $35 and RenderScreenshot Growth is $40 for 12,000 credits.
Can I keep my ApiFlash parameters when I switch?
Most map one to one, but three change meaning: delay is seconds on ApiFlash and milliseconds on RenderScreenshot, the default format is JPEG on ApiFlash and PNG on RenderScreenshot, and the default viewport is 1920x1080 on ApiFlash and 1280x800 on RenderScreenshot. Set all three explicitly.
How do I hide my API key in an img tag?
ApiFlash puts the access key in the query string and has no signing scheme, so a public img URL exposes the key. RenderScreenshot signed URLs carry a public key ID, an expiry (30 days at most) and an HMAC signature instead of the secret.
Does RenderScreenshot block cookie banners like ApiFlash's no_cookie_banners?
Yes. Set block.cookie_banners to true in a POST body, or block_cookies=true on a GET request. Test both services on your own cookie-heavy pages with the script on this page before you switch.

Related in this topic

Try RenderScreenshot free

150 free credits every month, no credit card required.