Disclosure: I build RenderScreenshot, which competes with both. This page compares ApiFlash and screenshotlayer on their own terms, using only their published documentation and prices. I name RenderScreenshot once, near the end.
ApiFlash and screenshotlayer are the two budget URL-to-image APIs that developers most often shortlist together. Both put an access key in a GET query string and return an image. They are not equivalent. ApiFlash controls when the screenshot is taken (wait for a selector, wait for network idle, block ads and cookie banners), and it allows 20 requests per second. screenshotlayer gives you a cheaper 10,000-shot plan, GIF output and a URL-signing scheme, but only a fixed delay to control timing and a rate limit measured per minute. If your targets are modern sites with cookie banners and late-loading content, ApiFlash. If you capture simple pages at modest rates and want the lowest bill at 10,000 a month, screenshotlayer.
This page is part of our comparison hub. The Urlbox vs ApiFlash comparison covers ApiFlash against a premium option instead.
Last verified: October 15, 2026, against the ApiFlash documentation, the pricing table on apiflash.com, screenshotlayer pricing and the screenshotlayer documentation. The live screenshotlayer documentation link redirected to an APILayer login page when we checked, so documentation facts come from the public copy archived on January 29, 2026.
The short answer
| Choose ApiFlash if | Choose screenshotlayer if |
|---|---|
Pages load content late, and you need wait_for (selector) or wait_until=network_idle |
Your targets are mostly static pages where a fixed delay is enough |
| You capture sites with cookie banners or ads | You want the lowest price at around 10,000 shots a month |
| You need more than 45 requests a minute | 30 to 45 requests a minute covers your peak |
| You need element capture, inline CSS or JS injection, or a transparent PNG | You need GIF output or a placeholder image while a capture is pending |
| You want quota headers on every response | You want screenshots exported to FTP |
Feature and parameter comparison
| Purpose | ApiFlash | screenshotlayer |
|---|---|---|
| Endpoint | GET/POST https://api.apiflash.com/v1/urltoimage |
GET https://api.screenshotlayer.com/api/capture |
| Auth | access_key in the query string or form body |
access_key in the query string, optional secret_key |
| Default viewport | 1920x1080 (width, height) |
1440x900 (viewport=1440x900), max 5000x5000 |
| Full page | full_page=true |
fullpage=1 |
| Scroll for lazy content | scroll_page=true |
not documented |
| Element | element (CSS selector) |
not documented |
| Crop | crop=left,top,width,height |
not documented |
| Thumbnail | thumbnail_width |
width (the thumbnail width; the viewport is a separate parameter) |
| Formats | jpeg (default), png, webp |
PNG (default), JPG, GIF; the pricing page lists WebP on paid plans |
| Retina | scale_factor=2 |
listed as "Retina/2x support" on paid plans |
| Wait for selector | wait_for (aborts after 15 s) |
not documented |
| Load condition | wait_until = dom_loaded, page_loaded, network_idle (default) |
not documented |
| Fixed delay | delay, seconds, max 10 |
delay, seconds, max 20 |
| Ads, trackers, cookie banners | no_ads, no_tracking, no_cookie_banners |
not documented |
| Custom CSS | css (inline string) |
css_url (URL of a stylesheet) |
| Custom JS | js |
not documented |
| Headers, cookies | headers, cookies, user_agent, accept_language |
user_agent, accept_lang |
| Cache | ttl (default 86400 s, max 30 days), fresh=true |
ttl (default 30 days, min 300 s), force=1 |
| Storage export | S3 (s3_bucket and related parameters) |
export to S3 or FTP (Professional and Enterprise per pricing page) |
| JSON response | response_type=json |
not documented; errors return a JSON body |
| Placeholder | no | placeholder (your image URL or 1 for the default) |
Two naming traps when you port between them. width on screenshotlayer is the thumbnail width, while on ApiFlash it is the viewport width; the screenshotlayer viewport is one WIDTHxHEIGHT string. And delay is seconds on both, but ApiFlash caps it at 10 and screenshotlayer at 20.
Timing control is the real difference
A screenshot API is mostly a decision about when to press the shutter. Too early and you capture a spinner, a half-loaded hero image or a cookie banner covering the page.
ApiFlash gives you three tools. wait_until=network_idle (its default) waits until no requests are in flight. wait_for=.price waits until a selector exists, which is the right tool for single-page apps. no_cookie_banners=true removes the banner that would otherwise cover the top of every EU site. The delay exists as a last resort, and the documentation says so.
screenshotlayer's documented tools are delay (a fixed wait in seconds) and css_url, which lets you host a stylesheet that hides known banner selectors. That works for pages you know. For arbitrary third-party URLs, a fixed delay is a guess: too short on slow pages, wasted time on fast ones, and at a rate limit of 30 to 45 requests a minute, wasted seconds add up.
Public URLs and the secret_key
Both APIs authenticate with a key in the query string, so neither URL can go into public HTML as it is. screenshotlayer offers a partial fix: secret_key, the MD5 hash of the target URL concatenated with a secret word that you set in the dashboard. We reproduced the example in their documentation to confirm the scheme:
// screenshotlayer secret_key = md5(target URL + secret word). Node 22, no dependencies. import { createHash } from 'node:crypto'; const secretKey = (targetUrl, secretWord) => createHash('md5').update(targetUrl + secretWord).digest('hex'); console.log(secretKey('http://www.apple.com', 'mysecretkeyword')); // a9d09c5b1cda7e5ef51fed4526e9cff3 (matches the documentation's example)
Tested on October 15, 2026 with Node 22.18.0.
What the hash does: it binds your access key to one target URL, so nobody can copy your <img src> and capture a different site on your quota. What it does not do: it never expires, and it does not cover viewport, fullpage, force or any other parameter. Anyone who copies the URL can request a 5000x5000 full-page capture with force=1 of that same target, as often as your rate limit allows. MD5 is also a weak primitive for new designs, although the bigger issue here is the missing expiry and parameter coverage.
ApiFlash has no signing at all, so keep ApiFlash URLs on the server and serve the image from your own storage or CDN.
Rate limits and quotas
| ApiFlash | screenshotlayer | |
|---|---|---|
| Documented rate | 20 requests/second, burst 400 | Free 2/min, Basic 30/min, Professional 45/min, Enterprise 45/min |
| Recommended pacing | none stated | at most 1 request every 2 seconds on paid plans |
| Over quota | HTTP 402 | overage billed per call (see pricing) |
| Repeated failures | identical failing calls limited to 5 per hour | not documented |
| Quota visibility | X-Quota-Limit, X-Quota-Remaining, X-Quota-Reset headers, /quota endpoint |
dashboard and e-mail notifications at 75%, 90% and 100% |
At 45 requests a minute, 10,000 screenshots take at least 3 hours 42 minutes of continuous requests. ApiFlash's documented rate does the same volume in minutes. If you render on demand when a user pastes a link, screenshotlayer's per-minute limit is the number to check first.
Pricing at three volumes
List prices, monthly billing, checked on October 15, 2026.
| Monthly volume | ApiFlash | screenshotlayer |
|---|---|---|
| 1,000 | $7 (Lite, 1,000) | $19.99 (Basic, 10,000); the free plan's 100 snapshots are for non-commercial use |
| 10,000 | $35 (Medium, 10,000) | $19.99 (Basic, 10,000) |
| 100,000 | $180 (Large, 100,000) | $149.99 (Enterprise, 75,000) plus 25,000 overage calls at about $0.008 each, roughly $350 in total |
| Free tier | 100/month | 100/month, non-commercial |
The screenshotlayer overage rate is our reading of the per-call price shown on the pricing page ($0.0079994667 on Enterprise). Their plan selector also pre-selects an annual "Platinum Support" add-on, so check the total at checkout. Yearly billing is up to 15% cheaper on screenshotlayer.
Test both on your URLs
We have not published head-to-head latency numbers for this pair; a fair run needs paid plans on both (screenshotlayer's free plan allows 2 requests a minute). The script below is what we use, written in Python with only the standard library. It sends the same viewport to both, bypasses both caches, checks that every response is really a PNG, and prints p50 and worst-case latency per URL.
"""headtohead.py: capture the same URLs with ApiFlash and screenshotlayer. Python 3.10+, standard library only. Usage: APIFLASH_KEY=... SCREENSHOTLAYER_KEY=... python3 headtohead.py urls.txt """ import csv import os import statistics import sys import time import urllib.error import urllib.parse import urllib.request RUNS = int(os.environ.get("RUNS", "3")) APIFLASH = os.environ.get("APIFLASH_BASE", "https://api.apiflash.com") SCREENSHOTLAYER = os.environ.get("SCREENSHOTLAYER_BASE", "https://api.screenshotlayer.com") def apiflash_url(target: str) -> str: query = urllib.parse.urlencode({ "access_key": os.environ["APIFLASH_KEY"], "url": target, "width": 1440, "height": 900, # match screenshotlayer's default viewport "format": "png", "fresh": "true", # bypass the cache: we want render time }) return f"{APIFLASH}/v1/urltoimage?{query}" def screenshotlayer_url(target: str) -> str: query = urllib.parse.urlencode({ "access_key": os.environ["SCREENSHOTLAYER_KEY"], "url": target, # urlencode() percent-encodes ? and & for us "viewport": "1440x900", "format": "PNG", "force": 1, # bypass the cache }) return f"{SCREENSHOTLAYER}/api/capture?{query}" def fetch(url: str) -> bytes: with urllib.request.urlopen(url, timeout=60) as res: body = res.read() ctype = res.headers.get("Content-Type", "") # screenshotlayer documents its errors as a JSON body ({"success": false, ...}), # so check the bytes instead of trusting the status code alone. if not body.startswith(b"\x89PNG"): raise ValueError(f"not a PNG ({ctype}): {body[:200]!r}") return body def main(path: str) -> int: targets = [line.strip() for line in open(path) if line.strip()] providers = {"apiflash": apiflash_url, "screenshotlayer": screenshotlayer_url} failures = 0 writer = csv.writer(sys.stdout) writer.writerow(["provider", "url", "runs", "ok", "p50_ms", "max_ms", "bytes"]) for target in targets: for name, build in providers.items(): times, size = [], 0 for run in range(RUNS): start = time.perf_counter() try: body = fetch(build(target)) except (urllib.error.URLError, ValueError) as err: failures += 1 print(f"{name} {target}: {err}", file=sys.stderr) continue times.append((time.perf_counter() - start) * 1000) size = len(body) if run == 0: safe = urllib.parse.quote(target, safe="") with open(f"{name}-{safe}.png", "wb") as fh: fh.write(body) p50 = round(statistics.median(times)) if times else "" worst = round(max(times)) if times else "" writer.writerow([name, target, RUNS, len(times), p50, worst, size]) return 1 if failures else 0 if __name__ == "__main__": sys.exit(main(sys.argv[1] if len(sys.argv) > 1 else "urls.txt"))
Tested on October 15, 2026 with Python 3.14.8 against a local mock of both endpoints (query encoding of a target with ? and &, PNG check, exit code). Not run against the live services.
For a fair result, pace the screenshotlayer calls to its documented rate (add time.sleep(2) between its requests on a paid plan), use URLs with content that loads after the load event, and compare the saved PNGs before the latency columns. If ApiFlash is slower but shows the content and screenshotlayer is faster but shows a spinner, the faster one lost.
What neither offers
Both services stop at "image from URL". Neither has expiring signed URLs that cover every parameter, presets for social card sizes, cache purge, batch jobs or PDF output. If you need those, RenderScreenshot has signed URLs with a maximum lifetime of 30 days, the og_card and other presets, and the wait parameters that screenshotlayer lacks. To test it on the same URLs, create a free key and add it as a third provider in the script above.