ScreenshotOne Alternative: Option Map and Shadow Test

A ScreenshotOne alternative compared honestly: option-by-option map, ScreenshotOne pricing at three volumes, and a shadow-mode migration in 3 languages.

By Simon Chiu · Updated

Disclosure: I build RenderScreenshot. ScreenshotOne is the most complete screenshot API on the market by option count, and this page lists the places where it is the better choice.

A ScreenshotOne alternative makes sense when you want use-case presets, a cache you can purge and share publicly by default, or lower prices at 10,000 shots a month. It does not make sense if you depend on video, animated screenshots, an MCP server for AI agents, or ScreenshotOne's long tail of options. This page maps ScreenshotOne's options to RenderScreenshot's, compares prices at three volumes, and gives you a shadow mode migration: keep serving ScreenshotOne while RenderScreenshot renders the same jobs in the background, then compare before you switch. For the wider market, see the best screenshot API comparison.

Last verified: October 15, 2026, against ScreenshotOne's option reference, caching docs and pricing page as published on October 14, 2026.

The short answer

Choose ScreenshotOne if Choose RenderScreenshot if
You need video, animated or scrolling screenshots, or GPU rendering You want presets for social cards, Twitter cards, mobile and HD instead of picking sizes
You want an MCP server so AI agents can take screenshots You embed cached images publicly and want a 24-hour default TTL, up to 30 days
You use options we do not have: full-page slices and algorithms, Markdown content extraction, OpenAI vision prompts, TIFF, GIF, HEIF, a device scale factor above 3 You need to purge cached images by key, URL pattern or date
You need 100,000+ shots a month on a self-serve plan You run about 10,000 shots a month and want the lower bill ($40 vs $79)
You want extra usage billed per screenshot instead of a hard stop You want a nested JSON body for complex requests and typed errors with a retryable flag

Option map

ScreenshotOne has well over 100 options, all flat (viewport_width, block_ads). RenderScreenshot groups them in nested JSON for POST (viewport.width, block.ads) and accepts flat names for the common ones on GET. The table covers the options most integrations use. A dash means there is no equivalent on our side today.

ScreenshotOne RenderScreenshot (POST body) Notes
access_key Authorization: Bearer rs_live_... Or api_key on GET, or a signed URL
signature (signed requests) key_id + expires + signature Ours must expire, 30 days at most
url / html / markdown url / html / markdown
viewport_width (default 1280) viewport.width (default 1280)
viewport_height (default 1024) viewport.height (default 800) Set it explicitly
device_scale_factor (1 to 5) viewport.scale (1 to 3)
viewport_device viewport.device or a preset Our presets also set format and cache TTL
viewport_mobile, viewport_has_touch, viewport_landscape viewport.mobile, viewport.touch, viewport.landscape
format (default jpg) output.format (default png) Both: PNG, JPEG, WebP, AVIF, PDF, HTML. Only ScreenshotOne: GIF, JP2, TIFF, HEIF, Markdown
image_quality (default 80) output.quality (default 80)
image_width, image_height output.resize.width, output.resize.height Plus resize.fit
omit_background output.transparent
full_page capture.mode: "full_page"
full_page_scroll, full_page_scroll_delay capture.scroll.simulate, capture.scroll.delay
full_page_max_height capture.scroll.max_height (default 16,000)
full_page_algorithm, full_page_slices -
selector capture.mode: "element" + capture.selector
error_on_selector_not_found capture.selector_required
clip_x, clip_y, clip_width, clip_height capture.clip
capture_beyond_viewport capture.beyond_viewport
wait_until (load, domcontentloaded, networkidle0, networkidle2) wait.until (load, domcontentloaded, networkidle) Our networkidle is 500 ms with no requests
delay (seconds) wait.delay (milliseconds) Multiply by 1,000
wait_for_selector wait.for_selector
timeout (default 60 s, max 90) wait.timeout (default 30 s, max 60)
block_ads, block_trackers block.ads, block.trackers
block_cookie_banners block.cookie_banners
block_chats block.chat_widgets
block_requests, block_resources block.requests, block.resources
hide_selectors page.hide or page.remove ScreenshotOne sets display: none, which is our page.remove
scripts, styles page.scripts, page.styles
click page.click
dark_mode, reduced_motion, media_type browser.dark_mode, browser.reduced_motion, browser.media
user_agent, time_zone browser.user_agent, browser.timezone
geolocation_latitude, _longitude, _accuracy browser.geolocation
headers, cookies, authorization network.headers, network.cookies, network.auth Cookies are objects on our side, not name=value; Domain=... strings
proxy, ip_country_code network.proxy, network.country
bypass_csp network.bypass_csp
cache, cache_ttl, cache_key cache.available, cache.ttl, cache.key Our TTL: 1 hour to 30 days, default 24 hours
store, storage_* storage.enabled, storage.path, storage.acl Our bucket credentials live in the dashboard
metadata_page_title, metadata_open_graph, metadata_fonts, metadata_http_response_status_code metadata.include: ["title", "og", "fonts", "status"]
fail_if_content_contains, fail_if_content_missing options.fail_if_contains, options.fail_if_missing
async + webhook_url webhook.url A webhook makes our request async
response_type=json Accept: application/json
openai_api_key, vision_prompt -

The full RenderScreenshot list is in the parameter docs. The ScreenshotOne migration guide walks through a real request conversion step by step.

What we have that ScreenshotOne does not

Fair claims only, checked against ScreenshotOne's docs:

  • Use-case presets. ScreenshotOne has device presets (viewport_device). RenderScreenshot has ten use-case presets that bundle size, format and cache TTL: og_card (1200×630), twitter_card (1200×600), twitter_card_large (1200×628), link_preview, full_page, pdf_document, mobile, mobile_landscape, desktop_hd and desktop_4k.
  • A cache meant for public links. Both return a direct cache link. ScreenshotOne's is opt-in, defaults to 4 hours, and its docs say it is "not intended to be used in CDN-like scenarios". Our X-Cache-URL comes on every response, defaults to 24 hours (up to 30 days), and is designed to go into public HTML. See caching.
  • Cache purge. DELETE /v1/cache/:key removes one image; POST /v1/cache/purge removes by keys, URL pattern or date. ScreenshotOne's caching docs offer a new cache_key or waiting for the TTL. See cache management.
  • Typed errors. Every RenderScreenshot error has a type, a code and a retryable boolean.

Price at three volumes

As listed on each pricing page on October 14, 2026, monthly billing:

Monthly volume ScreenshotOne RenderScreenshot
Free 100 screenshots 150 credits, no credit card
1,000 $17 (Basic, 2,000) $10 (Starter, 2,500 credits)
10,000 $79 (Growth, 10,000) $40 (Growth, 12,000 credits)
100,000 $459 (Scale $259 for 50,000, plus 50,000 × $0.004 extra) No self-serve plan; Scale is $100 for 60,000

ScreenshotOne does not count failed renders or cache hits. RenderScreenshot does not charge for cache hits requested with cache.available. Some ScreenshotOne features are plan-gated: IP location, scrolling screenshots and video start on Growth, GPU rendering on Scale. At 100,000 a month, ScreenshotOne has a self-serve answer and we do not. The pricing comparison has eight more vendors.

Head-to-head: what we measured

No numbers yet for either service. Both need paid keys, and I have not completed that run. I would rather show nothing than a guess. Every request on this page was tested against a local mock that checks paths, auth and parameter names for both APIs. To measure on your URLs, run the comparison script from the best screenshot API comparison:

node compare.mjs --providers screenshotone,renderscreenshot --urls urls.txt --runs 3 --out so-vs-rs.csv

The script sends cache=true to ScreenshotOne and cache.available to RenderScreenshot, so the "warm" rows measure each cache. The shadow mode below gives you a second, better data set: real production jobs, side by side.

Response and auth differences

  • Auth. ScreenshotOne: access_key in the query, plus optional signed requests. RenderScreenshot: Bearer header for server calls. Signed URLs use a separate public key ID (rs_pub_...) and secret (rs_secret_...), and must carry an expires timestamp.
  • Response. Both return the image by default. ScreenshotOne's metadata comes in x-screenshotone-* headers; ours in X-Screenshot-* headers, plus X-Cache-URL, X-Cache-Hit, X-Credits-Used and X-Credits-Remaining.
  • Defaults. Format JPEG vs PNG, height 1024 vs 800, delay seconds vs milliseconds, timeout 60 vs 30 seconds. Set each one explicitly in the adapter so output does not change shape on the day you switch.

Migration: shadow mode first

A shadow test answers "will my pages look right?" with your own traffic, before any user sees a RenderScreenshot image. ScreenshotOne keeps serving every request. When RS_SHADOW=1, the same job is sent to RenderScreenshot in the background and one JSON line is logged with sizes and timings. Set RS_ROLLOUT_PERCENT=100 to switch over.

Shadow mode costs a RenderScreenshot credit per job, so run it on a sample (one day, or a fraction of traffic) rather than forever.

Node.js

// shadow.mjs: keep serving ScreenshotOne, render the same job on RenderScreenshot in the background,
// and log one JSON line per comparison. Node 22+, npm i renderscreenshot.
import { Client, TakeOptions } from 'renderscreenshot';

const rs = new Client(process.env.RS_API_KEY);
const SHADOW = process.env.RS_SHADOW === '1';
const CUTOVER = process.env.RS_ROLLOUT_PERCENT === '100';

async function viaScreenshotOne(job) {
  const q = new URLSearchParams({
    access_key: process.env.SCREENSHOTONE_ACCESS_KEY,
    url: job.url,
    viewport_width: String(job.width),
    viewport_height: String(job.height),
    format: job.format,
    full_page: String(job.fullPage),
    block_cookie_banners: String(job.blockCookieBanners),
    dark_mode: String(job.darkMode),
  });
  const res = await fetch(`https://api.screenshotone.com/take?${q}`, { signal: AbortSignal.timeout(60_000) });
  if (!res.ok) throw new Error(`ScreenshotOne ${res.status}: ${await res.text()}`);
  return Buffer.from(await res.arrayBuffer());
}

function viaRenderScreenshot(job) {
  let o = TakeOptions.url(job.url).width(job.width).height(job.height).format(job.format);
  if (job.fullPage) o = o.fullPage();
  if (job.blockCookieBanners) o = o.blockCookieBanners();
  if (job.darkMode) o = o.darkMode();
  return rs.take(o);
}

const timed = async (fn) => { const t0 = performance.now(); const buf = await fn(); return { buf, ms: Math.round(performance.now() - t0) }; };

export async function screenshot(input) {
  const job = { width: 1280, height: 800, format: 'png', fullPage: false, blockCookieBanners: false, darkMode: false, ...input };
  if (CUTOVER) return viaRenderScreenshot(job);

  const primary = await timed(() => viaScreenshotOne(job));
  if (SHADOW) {
    // Fire and forget: the user never waits for the shadow call. It does cost one credit per job.
    timed(() => viaRenderScreenshot(job)).then(
      (s) => console.log(JSON.stringify({ shadow: 'ok', url: job.url, so_ms: primary.ms, rs_ms: s.ms, so_bytes: primary.buf.length, rs_bytes: s.buf.length })),
      (err) => console.log(JSON.stringify({ shadow: 'error', url: job.url, code: err.code ?? null, message: err.message })),
    );
  }
  return primary.buf;
}

if (import.meta.url === `file://${process.argv[1]}`) {
  const image = await screenshot({ url: process.argv[2] ?? 'https://example.com/', blockCookieBanners: true });
  console.log(`served ${image.length} bytes`);
}

Python

# shadow.py: serve ScreenshotOne, compare against RenderScreenshot in a worker thread.
# Python 3.9+. pip install httpx git+https://github.com/Render-Screenshot/rs-python.git
import json
import os
import sys
import time
from concurrent.futures import ThreadPoolExecutor

import httpx
from renderscreenshot import Client, TakeOptions

rs = Client(os.environ['RS_API_KEY'])
SHADOW = os.environ.get('RS_SHADOW') == '1'
CUTOVER = os.environ.get('RS_ROLLOUT_PERCENT') == '100'
pool = ThreadPoolExecutor(max_workers=4)


def via_screenshotone(url, width, height, fmt, full_page):
    params = {
        'access_key': os.environ['SCREENSHOTONE_ACCESS_KEY'],
        'url': url,
        'viewport_width': width,
        'viewport_height': height,
        'format': fmt,
        'full_page': str(full_page).lower(),
    }
    res = httpx.get('https://api.screenshotone.com/take', params=params, timeout=60)
    res.raise_for_status()
    return res.content


def via_renderscreenshot(url, width, height, fmt, full_page):
    options = TakeOptions.url(url).width(width).height(height).format(fmt)
    if full_page:
        options = options.full_page()
    return rs.take(options)


def compare(url, args, primary):
    start = time.monotonic()
    try:
        image = via_renderscreenshot(url, *args)
        print(json.dumps({'shadow': 'ok', 'url': url, 'so_bytes': len(primary), 'rs_bytes': len(image),
                          'rs_ms': round((time.monotonic() - start) * 1000)}))
    except Exception as exc:  # log every failure; never let the shadow path raise into the request
        print(json.dumps({'shadow': 'error', 'url': url, 'message': str(exc)}))


def screenshot(url, width=1280, height=800, fmt='png', full_page=False):
    args = (width, height, fmt, full_page)
    if CUTOVER:
        return via_renderscreenshot(url, *args)
    primary = via_screenshotone(url, *args)
    if SHADOW:
        pool.submit(compare, url, args, primary)
    return primary


if __name__ == '__main__':
    image = screenshot(sys.argv[1] if len(sys.argv) > 1 else 'https://example.com/')
    print(f'served {len(image)} bytes')
    pool.shutdown(wait=True)

Ruby

# shadow.rb: serve ScreenshotOne, compare against RenderScreenshot in a background thread.
# Ruby 3.0+. gem install renderscreenshot
require 'json'
require 'net/http'
require 'renderscreenshot'

RS = RenderScreenshot.client(ENV.fetch('RS_API_KEY'))
SHADOW = ENV['RS_SHADOW'] == '1'
CUTOVER = ENV['RS_ROLLOUT_PERCENT'] == '100'

def via_screenshotone(url, width:, height:, format:, full_page:)
  uri = URI('https://api.screenshotone.com/take')
  uri.query = URI.encode_www_form(
    access_key: ENV.fetch('SCREENSHOTONE_ACCESS_KEY'), url: url,
    viewport_width: width, viewport_height: height, format: format, full_page: full_page
  )
  res = Net::HTTP.get_response(uri)
  raise "ScreenshotOne #{res.code}: #{res.body}" unless res.is_a?(Net::HTTPSuccess)

  res.body
end

def via_renderscreenshot(url, width:, height:, format:, full_page:)
  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, width: 1280, height: 800, format: 'png', full_page: false)
  args = { width: width, height: height, format: format, full_page: full_page }
  return via_renderscreenshot(url, **args) if CUTOVER

  primary = via_screenshotone(url, **args)
  if SHADOW
    Thread.new do
      image = via_renderscreenshot(url, **args)
      puts({ shadow: 'ok', url: url, so_bytes: primary.bytesize, rs_bytes: image.bytesize }.to_json)
    rescue StandardError => e
      puts({ shadow: 'error', url: url, message: e.message }.to_json)
    end
  end
  primary
end

if $PROGRAM_NAME == __FILE__
  image = screenshot(ARGV[0] || 'https://example.com/')
  puts "served #{image.bytesize} bytes"
  sleep 1 if SHADOW # let the demo thread finish before the script exits
end

Tested on October 15, 2026, with RS_SHADOW=1, at RS_ROLLOUT_PERCENT=0 and 100, against a local mock of both APIs: Node.js 22.18.0 with renderscreenshot 1.1.0, Python 3.14 with the rs-python SDK 1.0.0, Ruby 4.0.7 with the renderscreenshot gem 1.0.0. In each language the shadow call logged one {"shadow":"ok",...} line and the primary response was served first.

Reading the shadow log

  1. Group by URL. Look at every row with "shadow":"error" first; the code field tells you whether it was a timeout, a blocked page or a bad parameter.
  2. Compare so_bytes and rs_bytes. A large drop on our side often means a blank or half-loaded page: add wait.for_selector for that site.
  3. Compare so_ms and rs_ms at p50 and p95 per site, not as one average.
  4. Open a sample of images from both sides for your hardest pages.

When to stay with ScreenshotOne

  • You use video, animated screenshots, scrolling screenshots or GPU rendering. We have none of those.
  • Your AI agents use the ScreenshotOne MCP server. We do not have one.
  • You rely on options from the dash rows above, such as full-page slices, Markdown extraction or OpenAI vision prompts.
  • You are above 60,000 shots a month and want a self-serve plan with per-screenshot overage.
  • Your integration works and the bill is fine. Switching costs engineering time; do it for a feature or a saving you can name.

Run the shadow test

Create a free RenderScreenshot key (150 credits a month, no credit card), set RS_API_KEY and RS_SHADOW=1 in staging, and read the log after a day of real jobs. The free credits cover a shadow sample of about a hundred jobs.

Frequently asked questions

What does ScreenshotOne cost?
As listed on ScreenshotOne's pricing page on October 14, 2026: free for 100 screenshots a month, Basic $17 for 2,000, Growth $79 for 10,000 and Scale $259 for 50,000, with extra screenshots at $0.009, $0.006 and $0.004. Annual billing gives 2 months free.
Does ScreenshotOne have a cache URL like X-Cache-URL?
Yes. With cache=true, ScreenshotOne returns a direct link in the x-screenshotone-cache-url header. Its default cache TTL is 4 hours (up to one month), and its docs say the cache is not meant for CDN-like use. RenderScreenshot's X-Cache-URL defaults to 24 hours and is meant for public embedding.
Can I invalidate a cached screenshot on ScreenshotOne?
Its caching docs describe changing cache_key or waiting for the TTL; I found no purge endpoint. RenderScreenshot has DELETE /v1/cache/:key and POST /v1/cache/purge by keys, URL pattern or date.
Is the RenderScreenshot API compatible with ScreenshotOne's?
No. The concepts map closely, but names, defaults and units differ. ScreenshotOne's delay is in seconds and ours is in milliseconds, its default format is JPEG and ours is PNG, and its default viewport is 1280x1024 and ours is 1280x800.

Related in this topic

Try RenderScreenshot free

150 free credits every month, no credit card required.