ApiFlash vs screenshotlayer: Features, Limits and Cost

ApiFlash vs screenshotlayer compared: wait and blocking options, rate limits, URL signing, prices at 1k, 10k and 100k, and a Python script to test both.

By Simon Chiu · Updated

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.

Frequently asked questions

Is screenshotlayer cheaper than ApiFlash?
At 10,000 screenshots a month, yes: screenshotlayer Basic is $19.99 and ApiFlash Medium is $35, by list price on October 15, 2026. At 1,000 a month ApiFlash Lite ($7) is cheaper, because screenshotlayer's free plan is for non-commercial use. At 100,000, ApiFlash Large is $180; screenshotlayer Enterprise covers 75,000 for $149.99 and bills overage per call.
Can screenshotlayer wait for a selector or block cookie banners?
Its documentation lists a fixed delay (up to 20 seconds), custom CSS by URL and a placeholder image, but no wait-for-selector, network-idle wait, ad blocking or cookie-banner blocking. ApiFlash has all four (wait_for, wait_until, no_ads, no_cookie_banners).
How does screenshotlayer's secret_key protect a public URL?
The secret_key is an MD5 hash of the target URL plus a secret word you set in the dashboard. It stops people from capturing other URLs with your key, but it has no expiry and does not cover the other parameters, so a copied URL can be reused indefinitely with different viewport or full-page settings.
What are the rate limits?
ApiFlash documents 20 requests per second with a burst of 400. screenshotlayer documents 2 requests per minute on the free plan, 30 on Basic and 45 on Professional and Enterprise, and recommends at most one request every 2 seconds.

Related in this topic

Try RenderScreenshot free

150 free credits every month, no credit card required.