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:
- take every query parameter except
signature(this includeskey_idandexpires), - sort them by name,
- form-encode each name and value (
application/x-www-form-urlencoded: spaces become+), - join with
&and compute HMAC-SHA256 with yourrs_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.