Screenshot Machine Alternative: Mapping, Signing, Pricing

Leaving Screenshot Machine? Map dimension, device, zoom and cacheLimit, replace the MD5 hash with signed URLs, and compare cost at three volumes.

By Simon Chiu · Updated

Disclosure: RenderScreenshot is our product, and Screenshot Machine is a competitor. Screenshot Machine facts on this page come from its API documentation and pricing page, both checked on 2026-10-15.

Screenshot Machine is a simple, long-running GET API: one URL with key, url and a few options returns an image. Many teams use it straight inside <img> tags, protected by an MD5 hash of the target URL and a secret phrase. A move to RenderScreenshot keeps that GET-in-an-img-tag pattern but changes three things: the parameter names, the signing scheme, and how errors come back.

This page covers all three, with tested code that converts your existing Screenshot Machine option objects into signed RenderScreenshot URLs. For the wider market, see the screenshot API comparison.

TL;DR

Stay with Screenshot Machine if Move to RenderScreenshot if
You capture 50,000+ fresh screenshots a month and price is the main factor You need WebP, AVIF or PDF output, or quality control on lossy formats
You want a placeholder image instead of an HTTP error in failed <img> tags You want signed links that expire and cover every parameter, not only the URL
Your width is 1,920 px or less and 3 device types are enough You need wider viewports (up to 7,680 px), named devices, or scale factors from 1 to 3
GIF output matters to you You want ad, tracker, chat-widget and cookie-banner blocking without writing selectors

Parameter mapping

Screenshot Machine is GET-only. RenderScreenshot supports the same GET style for embedding and a POST JSON body for everything else. The middle column uses our GET query names from the GET endpoint reference; the right column gives the POST name where the option exists only there.

Screenshot Machine RenderScreenshot GET POST JSON / notes
key api_key, or a signed URL Authorization: Bearer header
url url url
dimension=1366x768 width=1366&height=768 viewport.width, viewport.height. SM caps width at 1,920; ours goes to 7,680.
dimension=1366xfull width=1366&full_page=true capture.mode: "full_page", add capture.scroll.simulate for lazy images
device=phone / tablet device=iphone_14_pro / ipad_pro_12 Named devices set viewport, scale and mobile flags. See devices.
format=jpg (default) / png / gif format=jpeg / png Also webp, avif, pdf. No GIF.
zoom=200 scale=2 viewport.scale, 1 to 3
delay=2000 delay=2000 wait.delay. Prefer wait.for_selector or wait.until: "networkidle".
cacheLimit=2 (days) cache_ttl=172800 (seconds) Different meaning, see below
cacheLimit=0 cache_refresh=true cache.refresh
click=.accept click=.accept page.click
hide=.a,.b hide=.a,.b page.hide (array)
selector=table capture.mode: "element", capture.selector
crop=x,y,w,h capture.clip: {x, y, width, height}
cookies=a%3D1 network.cookies (array of objects)
user-agent, accept-language browser.user_agent, browser.locale, or network.headers
hash=md5(url+secret) key_id, expires, signature HMAC-SHA256 over all parameters

cacheLimit vs cache_ttl. Screenshot Machine's cacheLimit says how old a cached image you accept (0 to 14 days, default 14). Our cache_ttl says how long we keep an image (1 hour to 30 days, default 24 hours). The mapping above keeps the same number of days, which gives the same freshness in practice. If you set cacheLimit=0 to force a fresh capture, use cache_refresh=true. Details are in caching.

From MD5 hash to signed URLs

Screenshot Machine's hash is md5(url + secret_phrase). It proves the target URL came from you, but it does not cover the other parameters, and it never expires. A copied link keeps working, and someone can change dimension or format on it.

A RenderScreenshot signed URL covers every query parameter and has an expires timestamp (30 days at most). The server rebuilds the string to sign like this:

  1. take every query parameter except signature (this includes key_id and expires),
  2. sort them by name,
  3. form-encode each name and value (application/x-www-form-urlencoded: spaces become +),
  4. join with & and compute HMAC-SHA256 with your rs_secret_ key, as lowercase hex.

If you emit the URL in that same sorted, encoded order, the string you sign is exactly the query you send. The code below does that.

Node.js

// Turn a Screenshot Machine option object into a signed RenderScreenshot GET URL
// that you can put straight into <img src> or og:image.
import { createHmac } from 'node:crypto';

const DEVICES = { phone: 'iphone_14_pro', tablet: 'ipad_pro_12' };

export function mapOptions(sm) {
  const [w, h] = String(sm.dimension ?? '1024x768').split('x');
  const p = { url: sm.url, width: w };
  if (h === 'full') p.full_page = 'true'; else p.height = h;
  if (DEVICES[sm.device]) p.device = DEVICES[sm.device];
  p.format = sm.format === 'png' ? 'png' : 'jpeg'; // Screenshot Machine defaults to jpg; gif has no equivalent
  if (sm.delay) p.delay = String(Math.min(Number(sm.delay), 10_000));
  if (sm.zoom && Number(sm.zoom) > 100) p.scale = String(Math.min(Number(sm.zoom) / 100, 3));
  if (sm.click) p.click = sm.click;
  if (sm.hide) p.hide = sm.hide; // comma-separated selectors, same as Screenshot Machine
  if (sm.cacheLimit !== undefined) {
    const days = Number(sm.cacheLimit);
    if (days === 0) p.cache_refresh = 'true';
    else p.cache_ttl = String(Math.min(Math.max(Math.round(days * 86400), 3600), 2_592_000));
  }
  return p;
}

// Same encoding as the server: keys sorted, application/x-www-form-urlencoded values.
const enc = (s) => encodeURIComponent(s)
  .replace(/[!'()*]/g, (c) => '%' + c.charCodeAt(0).toString(16).toUpperCase())
  .replace(/%20/g, '+');

export function signedUrl(params, { keyId, secret, ttlSeconds = 7 * 86400 }) {
  const all = { ...params, key_id: keyId, expires: String(Math.floor(Date.now() / 1000) + ttlSeconds) };
  const query = Object.keys(all).sort().map((k) => `${enc(k)}=${enc(all[k])}`).join('&');
  const signature = createHmac('sha256', secret).update(query).digest('hex');
  return `https://api.renderscreenshot.com/v1/screenshot?${query}&signature=${signature}`;
}

// Usage
const src = signedUrl(
  mapOptions({ url: 'https://example.com/pricing', dimension: '1366xfull', format: 'png', cacheLimit: '2' }),
  { keyId: process.env.RS_KEY_ID, secret: process.env.RS_SECRET_KEY },
);

encodeURIComponent leaves ! ' ( ) * unencoded, and form encoding does not, so the enc helper fixes those five characters. Skip that step and any target URL with a * or ( fails verification.

Python

"""Turn a Screenshot Machine option dict into a signed RenderScreenshot GET URL."""
import hashlib
import hmac
import time
from urllib.parse import urlencode

DEVICES = {"phone": "iphone_14_pro", "tablet": "ipad_pro_12"}


def map_options(sm: dict) -> dict:
    w, h = str(sm.get("dimension", "1024x768")).split("x")
    p = {"url": sm["url"], "width": w}
    if h == "full":
        p["full_page"] = "true"
    else:
        p["height"] = h
    if sm.get("device") in DEVICES:
        p["device"] = DEVICES[sm["device"]]
    p["format"] = "png" if sm.get("format") == "png" else "jpeg"
    if sm.get("delay"):
        p["delay"] = str(min(int(sm["delay"]), 10_000))
    if sm.get("zoom") and int(sm["zoom"]) > 100:
        p["scale"] = str(min(int(sm["zoom"]) / 100, 3))
    for key in ("click", "hide"):
        if sm.get(key):
            p[key] = sm[key]
    if "cacheLimit" in sm:
        days = float(sm["cacheLimit"])
        if days == 0:
            p["cache_refresh"] = "true"
        else:
            p["cache_ttl"] = str(min(max(round(days * 86400), 3600), 2_592_000))
    return p


def signed_url(params: dict, key_id: str, secret: str, ttl_seconds: int = 7 * 86400) -> str:
    all_params = {**params, "key_id": key_id, "expires": str(int(time.time()) + ttl_seconds)}
    query = urlencode(sorted(all_params.items()))  # sorted keys, form encoding: same as the server
    signature = hmac.new(secret.encode(), query.encode(), hashlib.sha256).hexdigest()
    return f"https://api.renderscreenshot.com/v1/screenshot?{query}&signature={signature}"

Ruby

# Turn a Screenshot Machine option hash into a signed RenderScreenshot GET URL.
require 'openssl'
require 'cgi'

DEVICES = { 'phone' => 'iphone_14_pro', 'tablet' => 'ipad_pro_12' }.freeze

def map_options(sm)
  w, h = sm.fetch(:dimension, '1024x768').to_s.split('x')
  p = { url: sm[:url], width: w }
  h == 'full' ? p[:full_page] = 'true' : p[:height] = h
  p[:device] = DEVICES[sm[:device]] if DEVICES.key?(sm[:device])
  p[:format] = sm[:format] == 'png' ? 'png' : 'jpeg'
  p[:delay] = [sm[:delay].to_i, 10_000].min.to_s if sm[:delay]
  p[:scale] = [sm[:zoom].to_i / 100.0, 3].min.to_s if sm[:zoom].to_i > 100
  p[:click] = sm[:click] if sm[:click]
  p[:hide] = sm[:hide] if sm[:hide]
  if sm.key?(:cacheLimit)
    days = sm[:cacheLimit].to_f
    if days.zero?
      p[:cache_refresh] = 'true'
    else
      p[:cache_ttl] = (days * 86_400).round.clamp(3600, 2_592_000).to_s
    end
  end
  p
end

def signed_url(params, key_id:, secret:, ttl_seconds: 7 * 86_400)
  all = params.merge(key_id: key_id, expires: (Time.now.to_i + ttl_seconds).to_s)
  query = all.sort_by { |k, _| k.to_s }.map { |k, v| "#{CGI.escape(k.to_s)}=#{CGI.escape(v.to_s)}" }.join('&')
  signature = OpenSSL::HMAC.hexdigest('SHA256', secret, query)
  "https://api.renderscreenshot.com/v1/screenshot?#{query}&signature=#{signature}"
end

Tested on 2026-10-15 with Node 22.18.0, Python 3.14 and Ruby 4.0.7. We fed the URLs from all three into a verifier that recomputes each signature the way our API server does (Rails Hash#to_query plus HMAC-SHA256), including target URLs with spaces, *, !, ~, ( and '. All signatures matched. We did not call the live API in this test.

Keep rs_secret_ keys on the server. Generate the signed URL when you render the page, and choose an expiry longer than the page's cache lifetime, so a cached HTML page never points at an expired image.

Errors: images vs status codes

Screenshot Machine answers an invalid request with an error image and an X-Screenshotmachine-Response header (invalid_hash, invalid_url, no_credits, invalid_selector and others). A failed <img> therefore still shows something.

RenderScreenshot answers with an HTTP error status (400, 401, 403, 422, 429, 500) and a JSON body that has type, code, message and retryable. See error codes. In an <img> tag this shows as a broken image. If you want a placeholder, add one line:

<img src="{{ signed_url }}" alt="Preview of example.com/pricing" width="1366" height="768"
     onerror="this.onerror=null; this.src='/images/preview-unavailable.png'">

For server-side calls, check the status before you store the body. Screenshot Machine integrations often skip this step, because the response was always an image.

Cost at three volumes

Prices as listed on Screenshot Machine's pricing page (EUR, VAT may apply) and on our pricing page (USD), checked on 2026-10-15. Screenshot Machine bills extra screenshots in blocks of 1,000, rounded down, and does not bill cached impressions.

Fresh screenshots per month Screenshot Machine RenderScreenshot
1,000 Basic, 9 EUR (2,500 included) Starter, $10 (2,500 credits)
10,000 Basic + 7,000 billed extras = 37 EUR, or Pro at 59 EUR (20,000) Growth, $40 (12,000 credits)
100,000 Enterprise 99 EUR + 50,000 extras at 0.002 EUR = 199 EUR More than Scale's 60,000 credits ($100); contact us

At 1,000 and 10,000 a month the two are close. At 100,000 a month Screenshot Machine is cheaper on list price. Both have a free tier: Screenshot Machine lists 100 fresh screenshots a month, and our free plan includes 150 credits every month with no credit card.

Our measurement plan

We have not published a head-to-head benchmark of the two services yet. When we run it, we will use 3 runs per URL on our shared test corpus (static page, lazy-image page, cookie banner, SPA, 20,000 px page), with cold and warm cache reported separately, p50 and p95 latency, success rate and output bytes. Until then, test your own URLs: build one signed URL with the code above, one Screenshot Machine URL with the same dimension, and load both in a page.

When Screenshot Machine is the better choice

  • Volume is high, pages are simple, and the lowest monthly bill wins.
  • You rely on error images so a failed thumbnail never looks broken.
  • You need GIF output.
  • Your integration is a CMS plugin or a no-code tool that already speaks Screenshot Machine's URL format.

If you need modern formats, blocking, wider viewports or links that expire, get a free key and convert one of your existing URLs with the snippet above.

Frequently asked questions

How do I replace Screenshot Machine's hash parameter?
Use a RenderScreenshot signed URL. It carries your public key ID (rs_pub_), an expiry time and an HMAC-SHA256 signature over all query parameters, so nobody can change the target URL or reuse the link after it expires.
What happens to Screenshot Machine's error images?
Screenshot Machine returns an error image and puts the error code in the X-Screenshotmachine-Response header. RenderScreenshot returns an HTTP error status with a JSON body. In an img tag that shows as a broken image, so add an onerror fallback if you want a placeholder.
Is there a RenderScreenshot equivalent of dimension=1024xfull?
Yes. Send width=1024 and full_page=true. Lazy-loaded images need scroll simulation, which is part of the full_page preset and the capture.scroll options on POST requests.
Which is cheaper at 100,000 screenshots a month?
Screenshot Machine, on its listed prices in October 2026. Its Enterprise plan plus overage comes to about 199 EUR for 100,000 fresh screenshots, which is above our largest self-serve plan.

Related in this topic

Try RenderScreenshot free

150 free credits every month, no credit card required.