To migrate from Urlbox, convert the options object you send to /v1/render/sync (or put in render links) into a RenderScreenshot request body, fix the two unit changes (timeout and wait_until), and drop the second request that downloads renderUrl: RenderScreenshot returns the image in the first response. This guide gives you a converter in Node.js, Python and Ruby that accepts Urlbox options in camelCase or snake_case and lists everything it could not carry over. For the decision itself (features, prices, when Urlbox is the better tool) see the Urlbox alternative page and the alternatives hub.
Tested on October 15, 2026: the converters with Node.js 22.18.0, Python 3.14.8 and Ruby 4.0.7 against fixed test vectors, and the Ruby send path and SDK examples against a local mock of the RenderScreenshot API. Urlbox option names, units and defaults are as listed in Urlbox's options reference on October 14, 2026.
Which Urlbox integration do you have?
| You use | What changes | Where |
|---|---|---|
Render links in <img> / og:image |
The link builder function | Urlbox alternative, "swap the link builder" |
POST /v1/render/sync from a server |
Options object, response handling | This guide |
POST /v1/render/async + webhook_url |
Options, plus a batch job or dashboard webhook | This guide, then async screenshots and webhooks |
The same request, before and after
Urlbox sync render. The default response is JSON with a temporary renderUrl, so a second request downloads the file:
curl -X POST https://api.urlbox.com/v1/render/sync \ -H "Authorization: Bearer $URLBOX_SECRET" -H "Content-Type: application/json" \ -d '{ "url": "example.com", "full_page": true, "retina": true, "timeout": 45000, "wait_until": "requestsfinished", "hide_cookie_banners": true, "block_images": true }'
RenderScreenshot POST. One request, image bytes in the body:
curl -X POST https://api.renderscreenshot.com/v1/screenshot \ -H "Authorization: Bearer $RS_API_KEY" -H "Content-Type: application/json" \ -d '{ "url": "https://example.com", "viewport": { "height": 1024, "scale": 2 }, "capture": { "mode": "full_page" }, "wait": { "until": "networkidle", "timeout": 45 }, "block": { "cookie_banners": true, "resources": ["image"] } }' --output page.png
The GET form covers the flat parameters; scale, full_page, timeout and block_cookies are among them:
curl "https://api.renderscreenshot.com/v1/screenshot?url=https%3A%2F%2Fexample.com&height=1024&scale=2&full_page=true&timeout=45&block_cookies=true" \ -H "Authorization: Bearer $RS_API_KEY" --output page.png
If your code expects JSON, add -H "Accept: application/json". The response then carries image.url (a CDN link) and cache.url, which replaces Urlbox's renderUrl.
Values that change
| Urlbox | RenderScreenshot | What changes |
|---|---|---|
timeout in ms (5,000 to 100,000, default 30,000) |
wait.timeout in seconds (max 60, default 30) |
Divide by 1,000 and round up; the converter clamps values above 60 |
delay in ms |
wait.delay in ms |
Same unit; the API caps it at 10,000 ms |
wait_until: domloaded, loaded, requestsfinished, mostrequestsfinished |
wait.until: domcontentloaded, load, networkidle |
mostrequestsfinished (2 open connections allowed) becomes the stricter networkidle |
height default 1024 |
viewport.height default 800 |
Send 1024 if you relied on the default |
retina: true |
viewport.scale: 2 |
Scale goes up to 3 |
thumb_width, thumb_height, img_fit |
output.resize.width, .height, .fit |
Same fit names: cover, contain, fill, inside, outside |
block_images, block_fonts, block_medias, block_scripts, block_styles |
block.resources: [...] |
One array instead of five booleans |
hide_cookie_banners, click_accept |
block.cookie_banners |
One option does both: clicks consent buttons and hides banners |
hide_selector: ".a, .b" (comma string) |
page.hide: [".a", ".b"] |
Both use visibility: hidden |
js, css (one string each) |
page.scripts, page.styles (arrays) |
Wrap in an array |
click (can repeat) |
page.click (one selector) |
For several clicks, use page.scripts |
header: ["Name=Value"] |
network.headers: {"Name": "Value"} |
Object |
cookie: ["name=v;Domain=.d.com"] |
network.cookies: [{name, value, domain}] |
Objects, domain required |
authorization: "Basic ..." |
network.auth |
Basic is decoded into username and password |
ttl default 30 days |
cache.ttl default 24 hours (1 h to 30 days) |
Set cache.ttl: 2592000 to keep long caching |
force: true |
cache.refresh: true |
|
pdf_page_size: "A4", pdf_orientation, pdf_background |
pdf.paper: "a4", pdf.landscape, pdf.print_background |
Lowercase paper names. See PDF options |
Options with no equivalent today, among them use_stealth, solve_captchas, gpu, video_*, llm_*, and SVG, MP4, WebM or Markdown output, come back as warnings from the converter.
The converter
Node.js
// urlbox-to-rs.mjs: convert a Urlbox options object (camelCase or snake_case) into a RenderScreenshot POST body. // Node 22+, no dependencies. Returns { body, warnings }. const snake = (k) => k.replace(/[A-Z]/g, (c) => `_${c.toLowerCase()}`); const list = (v) => (Array.isArray(v) ? v : String(v).split(',').map((s) => s.trim()).filter(Boolean)); const WAIT_UNTIL = { domloaded: 'domcontentloaded', loaded: 'load', requestsfinished: 'networkidle', mostrequestsfinished: 'networkidle' }; const RESOURCE = { block_images: 'image', block_fonts: 'font', block_medias: 'media', block_scripts: 'script', block_styles: 'stylesheet' }; const DIRECT = { width: 'viewport.width', height: 'viewport.height', quality: 'output.quality', transparent: 'output.transparent', thumb_width: 'output.resize.width', thumb_height: 'output.resize.height', img_fit: 'output.resize.fit', max_height: 'capture.scroll.max_height', wait_for: 'wait.for_selector', block_ads: 'block.ads', dark_mode: 'browser.dark_mode', reduced_motion: 'browser.reduced_motion', media: 'browser.media', user_agent: 'browser.user_agent', tz: 'browser.timezone', accept_lang: 'browser.locale', ttl: 'cache.ttl', force: 'cache.refresh', pdf_landscape: 'pdf.landscape', pdf_background: 'pdf.print_background', pdf_margin: 'pdf.margin', }; function put(body, path, value) { const keys = path.split('.'); const leaf = keys.pop(); keys.reduce((node, k) => (node[k] ??= {}), body)[leaf] = value; } function cookieFromString(raw, host) { // Urlbox format: "name=value;Domain=.example.com" const [pair, ...attrs] = raw.split(';').map((s) => s.trim()); const i = pair.indexOf('='); const cookie = { name: pair.slice(0, i), value: pair.slice(i + 1), domain: host }; for (const a of attrs) if (/^domain=/i.test(a)) cookie.domain = a.slice(7).replace(/^\./, ''); return cookie; } export function fromUrlbox(input) { const opts = Object.fromEntries(Object.entries(input).map(([k, v]) => [snake(k), v])); const body = {}; const warnings = []; const host = opts.url ? new URL(/^https?:/.test(opts.url) ? opts.url : `https://${opts.url}`).hostname : undefined; for (const [key, v] of Object.entries(opts)) { if (DIRECT[key]) { put(body, DIRECT[key], v); continue; } if (RESOURCE[key]) { if (v) (body.block ??= {}).resources = [...(body.block?.resources ?? []), RESOURCE[key]]; continue; } switch (key) { case 'url': body.url = /^https?:/.test(v) ? v : `https://${v}`; break; // Urlbox accepts bare hosts case 'html': body.html = v; break; case 'format': { const f = v === 'jpg' ? 'jpeg' : v; if (['png', 'jpeg', 'webp', 'avif', 'pdf', 'html'].includes(f)) put(body, 'output.format', f); else warnings.push(`format ${v} has no equivalent (svg, mp4, webm and md are Urlbox-only)`); break; } case 'full_page': if (v) put(body, 'capture.mode', 'full_page'); break; case 'selector': put(body, 'capture.mode', 'element'); put(body, 'capture.selector', v); break; case 'fail_if_selector_missing': if (opts.selector) put(body, 'capture.selector_required', Boolean(v)); else warnings.push('fail_if_selector_missing only carries over for selector captures'); break; case 'retina': if (v) put(body, 'viewport.scale', 2); break; case 'delay': put(body, 'wait.delay', Math.min(Number(v), 10_000)); if (v > 10_000) warnings.push('delay capped at 10,000 ms'); break; case 'timeout': put(body, 'wait.timeout', Math.min(Math.ceil(Number(v) / 1000), 60)); break; // ms -> s case 'wait_until': put(body, 'wait.until', WAIT_UNTIL[v] ?? 'load'); if (v === 'mostrequestsfinished') warnings.push('mostrequestsfinished mapped to networkidle (stricter: 0 connections)'); break; case 'hide_cookie_banners': case 'click_accept': if (v) put(body, 'block.cookie_banners', true); break; case 'block_urls': put(body, 'block.requests', list(v)); break; case 'hide_selector': put(body, 'page.hide', list(v)); break; // both use visibility: hidden case 'css': put(body, 'page.styles', [v]); break; case 'js': put(body, 'page.scripts', [v]); break; case 'click': { const clicks = list(v); put(body, 'page.click', clicks[0]); if (clicks.length > 1) warnings.push(`only the first click carries over (${clicks.length} given)`); break; } case 'header': put(body, 'network.headers', Object.fromEntries(list(v).map((h) => [h.slice(0, h.indexOf('=')), h.slice(h.indexOf('=') + 1)]))); break; case 'cookie': put(body, 'network.cookies', [].concat(v).map((c) => cookieFromString(c, host))); break; case 'authorization': { const [scheme, token] = String(v).split(' '); if (/^bearer$/i.test(scheme)) put(body, 'network.auth', { type: 'bearer', token }); else if (/^basic$/i.test(scheme)) { const [username, ...rest] = Buffer.from(token, 'base64').toString('utf8').split(':'); put(body, 'network.auth', { type: 'basic', username, password: rest.join(':') }); } break; } case 'latitude': case 'longitude': case 'accuracy': put(body, `browser.geolocation.${key}`, Number(v)); break; case 'pdf_page_size': put(body, 'pdf.paper', String(v).toLowerCase()); break; case 'pdf_orientation': put(body, 'pdf.landscape', v === 'landscape'); break; case 'webhook_url': warnings.push('webhook_url: set the webhook in the dashboard, or pass webhook.url on POST /v1/batch'); break; case 'response_type': if (v === 'json') warnings.push('response_type=json: send Accept: application/json'); break; default: warnings.push(`${key} has no RenderScreenshot equivalent; dropped`); } } return { body, warnings }; } if (import.meta.url === `file://${process.argv[1]}`) { console.log(JSON.stringify(fromUrlbox({ url: 'example.com', fullPage: true, retina: true, timeout: 45000, waitUntil: 'requestsfinished', hideCookieBanners: true, blockImages: true, blockFonts: true, useStealth: true }), null, 2)); }
The test file feeds it camelCase input, thumbnails, selectors, cookies, Basic auth and PDF options, and checks the exact output. All three tests passed.
Python
# urlbox_to_rs.py: convert Urlbox options (camelCase or snake_case) into a RenderScreenshot POST body. # Python 3.9+, standard library only. import base64 import re from urllib.parse import urlparse WAIT_UNTIL = {'domloaded': 'domcontentloaded', 'loaded': 'load', 'requestsfinished': 'networkidle', 'mostrequestsfinished': 'networkidle'} RESOURCE = {'block_images': 'image', 'block_fonts': 'font', 'block_medias': 'media', 'block_scripts': 'script', 'block_styles': 'stylesheet'} DIRECT = { 'width': 'viewport.width', 'height': 'viewport.height', 'quality': 'output.quality', 'transparent': 'output.transparent', 'thumb_width': 'output.resize.width', 'thumb_height': 'output.resize.height', 'img_fit': 'output.resize.fit', 'max_height': 'capture.scroll.max_height', 'wait_for': 'wait.for_selector', 'block_ads': 'block.ads', 'dark_mode': 'browser.dark_mode', 'reduced_motion': 'browser.reduced_motion', 'media': 'browser.media', 'user_agent': 'browser.user_agent', 'tz': 'browser.timezone', 'accept_lang': 'browser.locale', 'ttl': 'cache.ttl', 'force': 'cache.refresh', 'pdf_background': 'pdf.print_background', 'pdf_margin': 'pdf.margin', } FORMATS = {'png', 'jpeg', 'webp', 'avif', 'pdf', 'html'} def snake(key): return re.sub(r'[A-Z]', lambda m: '_' + m.group(0).lower(), key) def as_list(value): return value if isinstance(value, list) else [s.strip() for s in str(value).split(',') if s.strip()] def put(body, path, value): *parents, leaf = path.split('.') node = body for p in parents: node = node.setdefault(p, {}) node[leaf] = value def from_urlbox(options): opts = {snake(k): v for k, v in options.items()} body, warnings = {}, [] if 'url' in opts and not opts['url'].startswith(('http://', 'https://')): opts['url'] = 'https://' + opts['url'] # Urlbox accepts bare hosts host = urlparse(opts['url']).hostname if 'url' in opts else None for key, v in opts.items(): if key in DIRECT: put(body, DIRECT[key], v) elif key in RESOURCE: if v: body.setdefault('block', {}).setdefault('resources', []).append(RESOURCE[key]) elif key in ('url', 'html'): body[key] = v elif key == 'format': f = 'jpeg' if v == 'jpg' else v if f in FORMATS: put(body, 'output.format', f) else: warnings.append(f'format {v} has no equivalent') elif key == 'full_page': if v: put(body, 'capture.mode', 'full_page') elif key == 'selector': put(body, 'capture.mode', 'element'); put(body, 'capture.selector', v) elif key == 'fail_if_selector_missing' and 'selector' in opts: put(body, 'capture.selector_required', bool(v)) elif key == 'retina': if v: put(body, 'viewport.scale', 2) elif key == 'delay': put(body, 'wait.delay', min(int(v), 10_000)) elif key == 'timeout': put(body, 'wait.timeout', min(-(-int(v) // 1000), 60)) # ms -> s, rounded up elif key == 'wait_until': put(body, 'wait.until', WAIT_UNTIL.get(v, 'load')) elif key in ('hide_cookie_banners', 'click_accept'): if v: put(body, 'block.cookie_banners', True) elif key == 'block_urls': put(body, 'block.requests', as_list(v)) elif key == 'hide_selector': put(body, 'page.hide', as_list(v)) elif key in ('css', 'js'): put(body, 'page.styles' if key == 'css' else 'page.scripts', [v]) elif key == 'click': clicks = as_list(v) put(body, 'page.click', clicks[0]) if len(clicks) > 1: warnings.append('only the first click carries over') elif key == 'header': put(body, 'network.headers', dict(h.split('=', 1) for h in as_list(v))) elif key == 'cookie': cookies = [] for raw in (v if isinstance(v, list) else [v]): pair, *attrs = [s.strip() for s in raw.split(';')] name, _, value = pair.partition('=') domain = next((a[7:].lstrip('.') for a in attrs if a.lower().startswith('domain=')), host) cookies.append({'name': name, 'value': value, 'domain': domain}) put(body, 'network.cookies', cookies) elif key == 'authorization': scheme, _, token = str(v).partition(' ') if scheme.lower() == 'bearer': put(body, 'network.auth', {'type': 'bearer', 'token': token}) elif scheme.lower() == 'basic': user, _, password = base64.b64decode(token).decode().partition(':') put(body, 'network.auth', {'type': 'basic', 'username': user, 'password': password}) elif key in ('latitude', 'longitude', 'accuracy'): put(body, f'browser.geolocation.{key}', float(v)) elif key == 'pdf_page_size': put(body, 'pdf.paper', str(v).lower()) elif key == 'pdf_orientation': put(body, 'pdf.landscape', v == 'landscape') else: warnings.append(f'{key} has no RenderScreenshot equivalent; dropped') return body, warnings if __name__ == '__main__': import json body, warnings = from_urlbox({'url': 'example.com', 'fullPage': True, 'retina': True, 'timeout': 45000, 'waitUntil': 'requestsfinished', 'hideCookieBanners': True, 'blockImages': True, 'useStealth': True}) print(json.dumps(body, indent=2)); print(warnings) assert body == {'url': 'https://example.com', 'capture': {'mode': 'full_page'}, 'viewport': {'scale': 2}, 'wait': {'timeout': 45, 'until': 'networkidle'}, 'block': {'cookie_banners': True, 'resources': ['image']}} b2, _ = from_urlbox({'url': 'https://app.example.com', 'hide_selector': '.a, #b', 'header': ['X-Preview=1'], 'cookie': 'session=abc;Domain=.example.com', 'thumbWidth': 400, 'imgFit': 'cover', 'pdfPageSize': 'A4', 'pdfOrientation': 'landscape'}) assert b2['page'] == {'hide': ['.a', '#b']} assert b2['network'] == {'headers': {'X-Preview': '1'}, 'cookies': [{'name': 'session', 'value': 'abc', 'domain': 'example.com'}]} assert b2['output'] == {'resize': {'width': 400, 'fit': 'cover'}} assert b2['pdf'] == {'paper': 'a4', 'landscape': True} print('ok')
Ruby
This version covers the options most server-side integrations use and includes the send step. Anything else becomes a warning.
# urlbox_to_rs.rb: convert Urlbox options (camelCase or snake_case) into a RenderScreenshot POST body, # then send it. Ruby 3.0+, standard library only. Covers the common options; others become warnings. require 'json' require 'net/http' module UrlboxToRs WAIT_UNTIL = { 'domloaded' => 'domcontentloaded', 'loaded' => 'load', 'requestsfinished' => 'networkidle', 'mostrequestsfinished' => 'networkidle' }.freeze RESOURCE = { 'block_images' => 'image', 'block_fonts' => 'font', 'block_medias' => 'media', 'block_scripts' => 'script', 'block_styles' => 'stylesheet' }.freeze DIRECT = { 'width' => 'viewport.width', 'height' => 'viewport.height', 'quality' => 'output.quality', 'thumb_width' => 'output.resize.width', 'thumb_height' => 'output.resize.height', 'img_fit' => 'output.resize.fit', 'max_height' => 'capture.scroll.max_height', 'wait_for' => 'wait.for_selector', 'block_ads' => 'block.ads', 'dark_mode' => 'browser.dark_mode', 'user_agent' => 'browser.user_agent', 'tz' => 'browser.timezone', 'ttl' => 'cache.ttl', 'force' => 'cache.refresh' }.freeze module_function def put(body, path, value) *parents, leaf = path.split('.') parents.reduce(body) { |node, key| node[key] ||= {} }[leaf] = value end def convert(options) opts = options.to_h { |k, v| [k.to_s.gsub(/([A-Z])/) { "_#{Regexp.last_match(1).downcase}" }, v] } body = {} warnings = [] opts.each do |key, v| if DIRECT.key?(key) then put(body, DIRECT[key], v) elsif RESOURCE.key?(key) then ((body['block'] ||= {})['resources'] ||= []) << RESOURCE[key] if v else case key when 'url' then body['url'] = v.match?(%r{\Ahttps?://}) ? v : "https://#{v}" when 'format' then put(body, 'output.format', v == 'jpg' ? 'jpeg' : v) when 'full_page' then put(body, 'capture.mode', 'full_page') if v when 'retina' then put(body, 'viewport.scale', 2) if v when 'delay' then put(body, 'wait.delay', [v.to_i, 10_000].min) when 'timeout' then put(body, 'wait.timeout', [(v.to_i / 1000.0).ceil, 60].min) # ms -> s when 'wait_until' then put(body, 'wait.until', WAIT_UNTIL.fetch(v, 'load')) when 'hide_cookie_banners', 'click_accept' then put(body, 'block.cookie_banners', true) if v when 'block_urls' then put(body, 'block.requests', Array(v)) when 'hide_selector' then put(body, 'page.hide', v.split(',').map(&:strip)) when 'css' then put(body, 'page.styles', [v]) when 'js' then put(body, 'page.scripts', [v]) when 'header' then put(body, 'network.headers', Array(v).to_h { |h| h.split('=', 2) }) else warnings << "#{key} has no RenderScreenshot equivalent; dropped" end end end [body, warnings] end def render(options, api_key: ENV.fetch('RS_API_KEY'), base: ENV.fetch('RS_API_BASE', 'https://api.renderscreenshot.com')) body, warnings = convert(options) warnings.each { |w| warn "warning: #{w}" } uri = URI("#{base}/v1/screenshot") res = Net::HTTP.post(uri, body.to_json, 'Authorization' => "Bearer #{api_key}", 'Content-Type' => 'application/json') raise "RenderScreenshot #{res.code}: #{res.body}" unless res.is_a?(Net::HTTPSuccess) res.body end end if $PROGRAM_NAME == __FILE__ body, warnings = UrlboxToRs.convert(url: 'example.com', fullPage: true, retina: true, timeout: 45_000, waitUntil: 'requestsfinished', hideCookieBanners: true, blockImages: true, useStealth: true) expected = { 'url' => 'https://example.com', 'capture' => { 'mode' => 'full_page' }, 'viewport' => { 'scale' => 2 }, 'wait' => { 'timeout' => 45, 'until' => 'networkidle' }, 'block' => { 'cookie_banners' => true, 'resources' => ['image'] } } raise "mismatch: #{body}" unless body == expected && warnings.size == 1 image = UrlboxToRs.render({ url: 'https://example.com', width: 1200, height: 630, format: 'png' }) puts "ok, #{image.bytesize} bytes" end
ruby urlbox_to_rs.rb checks the conversion, then renders one page. Against the mock API it printed ok, 70 bytes.
Replace the download step
With Urlbox you read renderUrl from the JSON and download it before it expires. With RenderScreenshot, keep the bytes from the first response, or keep the URL:
- Need the file now (save to disk, attach to an email): use the response body.
- Need a URL (store in a database, send to a browser): read
X-Cache-URL, a public CDN link valid for the cache TTL. For a link that never depends on our cache, enable your own storage. - Need a link in HTML before any render exists: build a signed URL. It always has an expiry, 30 days at most.
SDK examples
The converted example request in each official SDK. Options without a builder method (page scripts, PDF margins, resize) go in the POST body.
Node.js
import { Client, TakeOptions } from 'renderscreenshot'; const client = new Client(process.env.RS_API_KEY); const image = await client.take( TakeOptions.url('https://example.com').width(1280).height(1024).scale(2) .fullPage().waitFor('networkidle').blockCookieBanners().blockResources(['image', 'font']) );
Python
client = Client(os.environ['RS_API_KEY']) image = client.take( TakeOptions.url('https://example.com').width(1280).height(1024).scale(2) .full_page().wait_for('networkidle').block_cookie_banners().block_resources(['image', 'font']) )
Ruby
client = RenderScreenshot.client(ENV.fetch('RS_API_KEY')) image = client.take( RenderScreenshot::TakeOptions.url('https://example.com').width(1280).height(1024).scale(2) .full_page.wait_for('networkidle').block_cookie_banners.block_resources(%w[image font]) )
PHP
$client = new RenderScreenshot\Client(getenv('RS_API_KEY')); $image = $client->take( RenderScreenshot\TakeOptions::url('https://example.com')->width(1280)->height(1024)->scale(2) ->fullPage()->waitFor('networkidle')->blockCookieBanners()->blockResources(['image', 'font']) );
Java
Client client = new Client(System.getenv("RS_API_KEY")); byte[] image = client.take( TakeOptions.url("https://example.com").width(1280).height(1024).scale(2.0) .fullPage().waitFor("networkidle").blockCookieBanners() .blockResources(Arrays.asList("image", "font")) );
Go
image, err := client.Take(ctx, rs.URL("https://example.com").Width(1280).Height(1024).Scale(2). FullPage().WaitFor(rs.WaitNetworkIdle).BlockCookieBanners().BlockResources([]string{"image", "font"}))
Node.js, Python, Ruby, PHP and Go ran against the mock API. Java follows the Java SDK reference and was not run.
Async renders and webhooks
Urlbox's async endpoint returns at once and posts the result to webhook_url. RenderScreenshot has two ways to do the same job:
- Many URLs: send them to
POST /v1/batchwithwebhook.urlin the body. You get abatch.completedevent with aresults_url. See bulk screenshots. - Every screenshot: set one webhook URL in Dashboard, Webhooks. Each render then also sends
screenshot.completedorscreenshot.failed. Single requests still answer synchronously.
Both are signed with HMAC-SHA256 in the X-Webhook-Signature header. The verification code is in async screenshots and webhooks.
Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
Renders fail with a timeout error long after you expected |
Urlbox milliseconds copied into wait.timeout, which is seconds |
Divide by 1,000; 60 at most |
| Captures happen earlier than on Urlbox | wait_until value not recognized and fell back to load |
Map requestsfinished to networkidle |
invalid_url for example.com |
Urlbox accepts bare hosts; we need a scheme | Prefix https:// |
| Cookie banner still visible | Urlbox click_accept worked on a banner ours misses |
Add page.click with the accept button selector. See block ads and cookie banners |
| Only the first of several clicks happens | page.click takes one selector |
Do the extra clicks in page.scripts |
| Cached images expire after a day | Urlbox default TTL is 30 days, ours 24 hours | Set cache.ttl up to 2,592,000 |
| JSON parsing fails on the response | Code expects Urlbox's JSON default | Send Accept: application/json, or treat the body as image bytes |
| Thumbnails are cropped differently | img_fit not passed |
Set output.resize.fit to the same value |
Related
- Urlbox alternative: pricing, render links vs signed URLs, when to stay.
- Migrate from ScreenshotOne: the same process for query-string APIs.
- Signed URLs: the replacement for render links.
- Start with a free API key and convert one request.