Disclosure: I build RenderScreenshot. Urlbox is a mature, feature-rich API, and for several jobs it is the better choice. This page says which.
The most common Urlbox setup is a render link: an image URL, signed on your server, that you drop into an <img> or og:image tag. RenderScreenshot does the same thing with signed URLs. So a Urlbox alternative is mostly a question of three things: whether the options you use exist on the other side, what you pay per real render, and whether your links can live with an expiry date. This page answers all three, and gives you link builders in Node, Python and Ruby that produce valid links for both services. For the wider market, see the best screenshot API comparison.
Last verified: October 15, 2026, against Urlbox's render link docs, API reference, options reference and pricing page as published on October 14, 2026.
The short answer
| Choose Urlbox if | Choose RenderScreenshot if |
|---|---|
| Your image links must never expire (static sites, emails archived for years) | You can regenerate links at build or request time, and want them to expire |
| You render video (MP4, WebM), SVG or Markdown, or use LLM extraction | You want presets such as og_card and twitter_card_large |
| You need stealth, captcha solving, GPU or certified archives | You want a free plan to start (150 credits a month) instead of a 7-day trial |
| You want an SLA and a Business or Enterprise contract | You run up to 60,000 shots a month and want to pay per screenshot, not per 30 seconds |
| You need very long renders (up to 300 s on Ultra) | You want an API to purge cached images |
Render links vs signed URLs
Both are HMAC-SHA256 signatures computed on your server, so your secret never reaches the browser. The details differ:
| Urlbox render link | RenderScreenshot signed URL | |
|---|---|---|
| Shape | https://api.urlbox.com/v1/{api_key}/{token}/{format}?{options} |
https://api.renderscreenshot.com/v1/screenshot?{params}&signature=... |
| What is signed | The query string, as written | Every query parameter except signature, sorted by name and form-encoded |
| Key material | Project secret | rs_secret_... key; the URL carries the rs_pub_... key ID |
| Expiry | None in the token | expires is required, 30 days at most |
| Can you force signing? | Yes, "Force Secure Render Links" in project settings | Signed URLs are always signed; Bearer keys are for server calls |
| Cache | Render links cached 30 days by default (ttl to change) |
cache.ttl 1 hour to 30 days, default 24 hours; public X-Cache-URL on every response |
The canonical-string trap. Our API rebuilds the string it verifies from the query it receives: names sorted, values form-encoded, spaces as +. If your code encodes differently (JavaScript's encodeURIComponent leaves !'()* unescaped, for example), the link fails with invalid_signature only for URLs that contain those characters. The builders below handle it. I checked all three against a copy of the exact verification routine our API runs, with plain URLs and with a URL containing spaces, an apostrophe, parentheses, *, ~ and a query string: every link verified.
Option map
Urlbox accepts options in camelCase or snakecase; the table uses snakecase. A dash means no equivalent today.
| Urlbox | RenderScreenshot (POST body) |
Notes |
|---|---|---|
url, html |
url, html |
|
format (png, jpg, webp, pdf, svg, mp4, webm, md) |
output.format (png, jpeg, webp, avif, pdf, html) |
No video, SVG or Markdown output on our side |
width, height |
viewport.width, viewport.height |
|
retina |
viewport.scale: 2 |
We go up to 3 |
full_page, max_height |
capture.mode: "full_page", capture.scroll.max_height |
|
selector |
capture.mode: "element", capture.selector |
|
clip |
capture.clip |
|
delay (ms) |
wait.delay (ms) |
Same unit |
timeout (ms, 5,000 to 100,000) |
wait.timeout (seconds, max 60) |
Divide by 1,000 |
wait_until (domloaded, loaded, requestsfinished, mostrequestsfinished) |
wait.until (domcontentloaded, load, networkidle) |
requestsfinished is closest to our networkidle |
wait_for |
wait.for_selector |
|
fail_if_selector_missing |
capture.selector_required (element captures) |
|
dark_mode, reduced_motion, media |
browser.dark_mode, browser.reduced_motion, browser.media |
|
block_ads |
block.ads |
|
hide_cookie_banners, click_accept |
block.cookie_banners |
|
block_urls |
block.requests |
|
block_images, block_fonts, block_medias, block_scripts, block_styles |
block.resources: ["image", "font", "media", "script", "stylesheet"] |
|
hide_selector |
page.hide |
|
css, js |
page.styles, page.scripts |
|
click, hover |
page.click, - |
|
user_agent, header, cookie |
browser.user_agent, network.headers, network.cookies |
|
accept_lang, tz |
browser.locale, browser.timezone |
|
latitude, longitude, accuracy |
browser.geolocation |
|
proxy |
network.proxy |
|
thumb_width, thumb_height |
output.resize.width, output.resize.height |
|
transparent, quality |
output.transparent, output.quality |
|
pdf_page_size, pdf_orientation, pdf_margin, pdf_background |
pdf.paper, pdf.landscape, pdf.margin, pdf.background |
|
ttl, force, unique |
cache.ttl, cache.refresh, cache.key |
|
webhook_url |
webhook.url |
|
save_metadata |
metadata.include |
|
use_stealth, solve_captchas, gpu, video_*, llm_* |
- |
Our full list: parameter docs. A request-by-request walkthrough is in the Urlbox migration guide.
Price at three volumes, and what a render is
As listed on each pricing page on October 14, 2026, monthly billing:
| Monthly volume | Urlbox | RenderScreenshot |
|---|---|---|
| Free | 7-day trial | 150 credits a month, no credit card |
| 1,000 | $19 (Lo-Fi, 2,000; low-quality thumbnails of third-party sites) or $49 (Hi-Fi, 5,000) | $10 (Starter, 2,500 credits) |
| 10,000 | $99 (Ultra, 15,000) | $40 (Growth, 12,000 credits) |
| 100,000 | $795 (Business: $495 base + $3 per 1,000) | No self-serve plan; Scale is $100 for 60,000 |
Urlbox's counting rules, from its pricing page footnotes: one render per 30 seconds of render time, one render per 5 MB of output, twice the renders with GPU, 3 renders per captcha solve on Urlbox's keys, managed proxies at their multiplier (2× for geo up to 50× for covert), and "advanced features may use additional renders". The REST sync endpoint is not cached, so a repeated call is a new render. On RenderScreenshot one fresh render is one credit, and a cache hit requested with cache.available is zero.
So for small social cards the plan numbers are close to real. For long full-page PNGs of heavy pages, Urlbox's count can be a multiple of the shot count. Check your own render times and file sizes before you compare plans. The pricing comparison explains the same issue for eight more vendors.
Head-to-head: what we measured
No latency or success numbers yet. Both services need keys for a real run, and I have not done it. What I did test is listed under each code block. To measure on your own URLs, use the script from the best screenshot API comparison; its Urlbox adapter calls the sync endpoint, so its "warm" rows are not cached on Urlbox's side:
node compare.mjs --providers urlbox,renderscreenshot --urls urls.txt --runs 3 --out urlbox-vs-rs.csv
Migration step 1: swap the link builder
Your templates call one function to get the image URL. Replace its body; the HTML does not change. Set URLBOX_SECRET and URLBOX_PUBLISHABLE_KEY for the old path, RS_PUBLIC_KEY_ID (rs_pub_...) and RS_SECRET_KEY (rs_secret_...) for the new one, and flip RS_ROLLOUT_PERCENT=100.
Node.js
// links.mjs: build the public og:image link for a page: Urlbox render link or RenderScreenshot signed URL. // Node 22+, no dependencies. Server-side only: both builders use secret keys. import { createHmac } from 'node:crypto'; const USE_RS = process.env.RS_ROLLOUT_PERCENT === '100'; function urlboxRenderLink(pageUrl) { const query = new URLSearchParams({ url: pageUrl, width: '1200', height: '630' }).toString(); const token = createHmac('sha256', process.env.URLBOX_SECRET).update(query).digest('hex'); return `https://api.urlbox.com/v1/${process.env.URLBOX_PUBLISHABLE_KEY}/${token}/png?${query}`; } // Form-encode like Ruby's CGI.escape: space becomes "+", and !'()* are escaped too. const enc = (v) => encodeURIComponent(v).replace(/[!'()*]/g, (c) => `%${c.charCodeAt(0).toString(16).toUpperCase()}`).replace(/%20/g, '+'); function renderScreenshotSignedUrl(pageUrl, days = 7) { const expires = Math.floor(Date.now() / 1000) + days * 86_400; // must be 30 days or less const params = { url: pageUrl, preset: 'og_card', key_id: process.env.RS_PUBLIC_KEY_ID, expires: String(expires) }; // Sign every query parameter except the signature, sorted by name. const canonical = Object.keys(params).sort().map((k) => `${k}=${enc(params[k])}`).join('&'); const signature = createHmac('sha256', process.env.RS_SECRET_KEY).update(canonical).digest('hex'); return `https://api.renderscreenshot.com/v1/screenshot?${canonical}&signature=${signature}`; } export function ogImageUrl(pageUrl) { return USE_RS ? renderScreenshotSignedUrl(pageUrl) : urlboxRenderLink(pageUrl); } if (import.meta.url === `file://${process.argv[1]}`) { const link = ogImageUrl(process.argv[2] ?? 'https://example.com/blog/hello?ref=og'); if (process.env.PRINT_FULL_LINK === '1') console.log(link); // test harness only const u = new URL(link); console.log(u.origin + u.pathname.replace(/[0-9a-f]{64}/, '<token>'), [...u.searchParams.keys()].join(',')); }
Python
# links.py: og:image link builders for Urlbox and RenderScreenshot. Server-side only. # Python 3.9+, standard library only. import hashlib import hmac import os import sys import time from urllib.parse import urlencode, urlsplit USE_RS = os.environ.get('RS_ROLLOUT_PERCENT') == '100' def urlbox_render_link(page_url): query = urlencode({'url': page_url, 'width': 1200, 'height': 630}) token = hmac.new(os.environ['URLBOX_SECRET'].encode(), query.encode(), hashlib.sha256).hexdigest() return f"https://api.urlbox.com/v1/{os.environ['URLBOX_PUBLISHABLE_KEY']}/{token}/png?{query}" def renderscreenshot_signed_url(page_url, days=7): expires = int(time.time()) + days * 86400 # must be 30 days or less params = {'url': page_url, 'preset': 'og_card', 'key_id': os.environ['RS_PUBLIC_KEY_ID'], 'expires': expires} canonical = urlencode(sorted(params.items())) # sorted by name, form-encoded signature = hmac.new(os.environ['RS_SECRET_KEY'].encode(), canonical.encode(), hashlib.sha256).hexdigest() return f'https://api.renderscreenshot.com/v1/screenshot?{canonical}&signature={signature}' def og_image_url(page_url): return renderscreenshot_signed_url(page_url) if USE_RS else urlbox_render_link(page_url) if __name__ == '__main__': link = og_image_url(sys.argv[1] if len(sys.argv) > 1 else 'https://example.com/blog/hello?ref=og') if os.environ.get('PRINT_FULL_LINK') == '1': print(link) # test harness only parts = urlsplit(link) print(f'{parts.scheme}://{parts.netloc}{parts.path[:40]}') # never log a full signed link
Ruby
# links.rb: og:image link builders for Urlbox and RenderScreenshot. Server-side only. # Ruby 3.0+, standard library only. require 'cgi' require 'openssl' require 'uri' USE_RS = ENV['RS_ROLLOUT_PERCENT'] == '100' def urlbox_render_link(page_url) query = URI.encode_www_form(url: page_url, width: 1200, height: 630) token = OpenSSL::HMAC.hexdigest('sha256', ENV.fetch('URLBOX_SECRET'), query) "https://api.urlbox.com/v1/#{ENV.fetch('URLBOX_PUBLISHABLE_KEY')}/#{token}/png?#{query}" end def renderscreenshot_signed_url(page_url, days: 7) expires = Time.now.to_i + (days * 86_400) # must be 30 days or less params = { 'url' => page_url, 'preset' => 'og_card', 'key_id' => ENV.fetch('RS_PUBLIC_KEY_ID'), 'expires' => expires } canonical = params.sort.map { |k, v| "#{k}=#{CGI.escape(v.to_s)}" }.join('&') signature = OpenSSL::HMAC.hexdigest('sha256', ENV.fetch('RS_SECRET_KEY'), canonical) "https://api.renderscreenshot.com/v1/screenshot?#{canonical}&signature=#{signature}" end def og_image_url(page_url) USE_RS ? renderscreenshot_signed_url(page_url) : urlbox_render_link(page_url) end if $PROGRAM_NAME == __FILE__ link = og_image_url(ARGV[0] || 'https://example.com/blog/hello?ref=og') puts link if ENV['PRINT_FULL_LINK'] == '1' # test harness only uri = URI(link) puts "#{uri.scheme}://#{uri.host}#{uri.path[0, 40]}" # never log a full signed link end
Tested on October 15, 2026 with Node.js 22.18.0, Python 3.14 and Ruby 4.0.7, standard libraries only. Each RenderScreenshot link was checked with a copy of the signature check in our API (Rails to_query over every parameter except signature, then HMAC-SHA256), for a plain URL and for one with spaces, ', (, ), *, ~ and a query string. All six links verified. The Urlbox builders follow the token recipe in Urlbox's render-link docs; I did not call Urlbox with a real key.
Expiry is the one behaviour change. A static site that bakes the link into HTML at build time needs a rebuild at least every 30 days, or a small route on your server that redirects to a fresh link. Social platforms cache og:image on their side, so an expired link mostly affects first shares after expiry. If neither fits, keep Urlbox for those pages.
Migration step 2: server-side renders
If you also call Urlbox's REST API to save images (thumbnails, archives, PDFs), the response shape changes: Urlbox returns JSON with a temporary renderUrl, and you download the file in a second request. RenderScreenshot returns the image bytes in the first response.
// render.mjs: server-side renders. Urlbox /v1/render/sync answers with JSON, RenderScreenshot with the image. // Node 22+, npm i renderscreenshot. import { Client, TakeOptions } from 'renderscreenshot'; const rs = new Client(process.env.RS_API_KEY); const USE_RS = process.env.RS_ROLLOUT_PERCENT === '100'; async function viaUrlbox({ url, width, height, fullPage }) { const res = await fetch('https://api.urlbox.com/v1/render/sync', { method: 'POST', headers: { Authorization: `Bearer ${process.env.URLBOX_SECRET}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ url, width, height, full_page: fullPage, format: 'png' }), signal: AbortSignal.timeout(120_000), }); if (!res.ok) throw new Error(`Urlbox ${res.status}: ${await res.text()}`); const { renderUrl } = await res.json(); // second request to download the file const file = await fetch(renderUrl); if (!file.ok) throw new Error(`Urlbox download ${file.status}`); return Buffer.from(await file.arrayBuffer()); } function viaRenderScreenshot({ url, width, height, fullPage }) { let o = TakeOptions.url(url).width(width).height(height).format('png'); if (fullPage) o = o.fullPage(); return rs.take(o); // one request, image bytes in the body } export function render(job) { const full = { width: 1280, height: 800, fullPage: false, ...job }; return USE_RS ? viaRenderScreenshot(full) : viaUrlbox(full); } if (import.meta.url === `file://${process.argv[1]}`) { const image = await render({ url: process.argv[2] ?? 'https://example.com/', fullPage: true }); console.log(`${image.length} bytes`); }
Tested on October 15, 2026 against a local mock of both APIs (Urlbox: JSON, then file download; RenderScreenshot: PNG bytes), with renderscreenshot 1.1.0. For Python and Ruby, the SDK pattern on the ApiFlash alternative page carries over unchanged: only the via_ function for the old provider differs.
When to stay with Urlbox
- Your links must outlive 30 days without a rebuild. Urlbox render links carry no expiry.
- You render video, SVG or Markdown, or use LLM extraction.
- Your targets fight bots. Stealth, captcha solving and managed proxies are built in on Urlbox's higher plans; we have proxies and country routing but not stealth or captcha solving.
- You need contracts: uptime SLAs, security reviews, regional or dedicated clusters, on-premises.
- You need renders longer than 60 seconds. Our
wait.timeoutmaxes out at 60.
Try the swap
Create a free RenderScreenshot key, create a public key and secret key pair in the dashboard, and point the link builder above at one staging page. Share the page in Slack or LinkedIn's post inspector and check the card renders.
Related
- Best screenshot API comparison: ten services on one checklist.
- ScreenshotOne alternative: the other option-rich API.
- Signed URLs: parameters, expiry and examples.
- Migrate from Urlbox: request-by-request conversion.