Urlbox Alternative: Render Links vs Signed URLs

A Urlbox alternative compared on facts: option mapping, Urlbox pricing and render multipliers, and tested code that swaps render links for signed URLs.

By Simon Chiu · Updated

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

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

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.timeout maxes 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.

Frequently asked questions

How much does Urlbox cost?
As listed on Urlbox's pricing page on October 14, 2026: Lo-Fi $19 for 2,000 renders (screenshots of sites you own), Hi-Fi $49 for 5,000, Ultra $99 for 15,000, Business $495 base plus $3 per 1,000 renders, and Enterprise from $3,000. There is a 7-day free trial and no free tier.
Is one Urlbox render the same as one screenshot?
Not always. Urlbox counts one render per 30 seconds of render time and per 5 MB of output, doubles renders for GPU, multiplies them for managed proxies (2x to 50x), and charges 3 renders per captcha solve.
Do RenderScreenshot signed URLs expire?
Yes, always, after 30 days at most. Urlbox render links have no expiry in the token. If your og:image URLs are baked into static HTML for months, Urlbox render links are simpler; with RenderScreenshot you regenerate the URL when you build or serve the page.
Can I replace Urlbox render links in my templates without changing the HTML?
Yes. Both are plain image URLs signed with HMAC-SHA256 on your server. Swap the function that builds the link; the img or meta tag stays the same.

Related in this topic

Try RenderScreenshot free

150 free credits every month, no credit card required.