To migrate from ScreenshotOne, convert each /take query string into a RenderScreenshot request once, check the few options that change unit or default, and send the result to POST /v1/screenshot with a Bearer key. This guide gives you a converter that does the conversion in Node.js, Python or Ruby and tells you what it could not carry over. If you are still deciding whether to switch, read the ScreenshotOne alternative page first; it compares features, prices and when to stay. The alternatives hub covers the other services.
Tested on October 15, 2026: the converter and its tests with Node.js 22.18.0, Python 3.14.8 and Ruby 4.0.7; the send script and every SDK example below against a local mock of the RenderScreenshot API. ScreenshotOne option names and defaults were checked against its options reference as published on October 14, 2026.
Before you start: list the requests you send
A ScreenshotOne integration keeps all its state in query strings. That makes the migration mechanical:
- Search your code and logs for
api.screenshotone.com/take. - Copy each distinct query string (without
access_key) into a file, one per line. - Run each line through the converter below and read the warnings.
Most apps have three to ten distinct option sets. Convert those, not every call site.
The same request, before and after
ScreenshotOne, with access_key in the URL:
curl "https://api.screenshotone.com/take?access_key=$SO_KEY&url=https%3A%2F%2Fexample.com&format=jpg&viewport_height=1024&delay=2&full_page=true&block_cookie_banners=true" --output before.jpg
RenderScreenshot GET, with the key in a header and delay in milliseconds:
curl "https://api.renderscreenshot.com/v1/screenshot?url=https%3A%2F%2Fexample.com&format=jpeg&height=1024&delay=2000&full_page=true&block_cookies=true" \ -H "Authorization: Bearer $RS_API_KEY" --output after.jpg
RenderScreenshot POST, the form the converter produces. Use it for anything beyond the flat GET parameters (cookies, headers, page scripts, clip, resize):
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 }, "output": { "format": "jpeg" }, "wait": { "delay": 2000 }, "capture": { "mode": "full_page" }, "block": { "cookie_banners": true } }' --output after.jpg
Both services return image bytes by default, so code that writes the response body to a file keeps working.
Values that change on the way over
Most options are a rename. These ones change a value, a unit or a shape, and they cause most migration bugs. The full option-by-option list is on the ScreenshotOne alternative page.
| ScreenshotOne | RenderScreenshot | What changes |
|---|---|---|
delay=2 (seconds) |
wait.delay: 2000 (ms) |
Multiply by 1,000. The API caps wait.delay at 10,000 ms. |
timeout (default 60, max 90 s) |
wait.timeout (default 30, max 60 s) |
The converter clamps values above 60. |
format default jpg |
output.format default png |
Send jpeg explicitly if you relied on the default. |
viewport_height default 1024 |
viewport.height default 800 |
Send 1024 if you relied on the default. |
device_scale_factor 1 to 5 |
viewport.scale 1 to 3 |
The converter clamps values above 3. |
wait_until=networkidle0 / networkidle2 |
wait.until: "networkidle" |
Ours waits for 500 ms with no requests. |
hide_selectors (sets display: none) |
page.remove |
page.hide uses visibility: hidden and keeps the space. |
cookies=name=v; Domain=d; Secure |
network.cookies: [{name, value, domain, secure}] |
Objects; domain is required. |
headers=Name:Value |
network.headers: {"Name": "Value"} |
Object instead of repeated strings. |
authorization=Basic ... |
network.auth: {type, username, password} |
Decode Basic credentials; Bearer becomes {type: "bearer", token}. |
cache=true |
cache.available: true |
A hit returns the earlier render for 0 credits. |
cache_ttl (min 4 h) |
cache.ttl (1 h to 30 days, default 24 h) |
Seconds on both sides. |
response_type=json |
Accept: application/json header |
A header, not a parameter. |
async=true + webhook_url |
Dashboard webhook, or POST /v1/batch |
Single screenshots answer synchronously. See async screenshots and webhooks. |
The converter
Each version takes a ScreenshotOne query string and returns the RenderScreenshot body plus a list of warnings. Unknown options are never silently passed through.
Node.js
// so-to-rs.mjs: translate a ScreenshotOne /take query string into a RenderScreenshot POST body. // Node 22+, no dependencies. Returns { body, warnings } so you can log what did not carry over. const bool = (v) => v === 'true' || v === '1'; const num = (v) => Number(v); function parseCookie(raw, fallbackDomain) { // ScreenshotOne format: "name=value; Domain=example.com; Secure; HttpOnly" const [pair, ...attrs] = raw.split(';').map((s) => s.trim()).filter(Boolean); const eq = pair.indexOf('='); const cookie = { name: pair.slice(0, eq), value: pair.slice(eq + 1), domain: fallbackDomain }; for (const attr of attrs) { const [k, v] = attr.split('='); const key = k.toLowerCase(); if (key === 'domain') cookie.domain = v; else if (key === 'path') cookie.path = v; else if (key === 'secure') cookie.secure = true; else if (key === 'httponly') cookie.http_only = true; else if (key === 'samesite') cookie.same_site = v; } return cookie; } function parseAuthorization(value) { const [scheme, token] = value.split(' '); if (scheme.toLowerCase() === 'bearer') return { type: 'bearer', token }; if (scheme.toLowerCase() === 'basic') { const decoded = Buffer.from(token, 'base64').toString('utf8'); const i = decoded.indexOf(':'); return { type: 'basic', username: decoded.slice(0, i), password: decoded.slice(i + 1) }; } return null; } export function fromScreenshotOne(query) { const q = new URLSearchParams(query); const body = {}; const warnings = []; const group = (name) => (body[name] ??= {}); for (const key of new Set(q.keys())) { const v = q.get(key); const all = q.getAll(key); switch (key) { case 'access_key': case 'signature': break; // auth moves to the Bearer header case 'url': case 'html': case 'markdown': body[key] = v; break; case 'viewport_width': group('viewport').width = num(v); break; case 'viewport_height': group('viewport').height = num(v); break; case 'device_scale_factor': group('viewport').scale = Math.min(num(v), 3); if (num(v) > 3) warnings.push(`device_scale_factor ${v} clamped to 3`); break; case 'viewport_mobile': group('viewport').mobile = bool(v); break; case 'viewport_has_touch': group('viewport').touch = bool(v); break; case 'viewport_landscape': group('viewport').landscape = bool(v); break; case 'format': { const f = v === 'jpg' ? 'jpeg' : v; if (['png', 'jpeg', 'webp', 'avif', 'pdf', 'html'].includes(f)) group('output').format = f; else warnings.push(`format ${v} is not supported; using png`); break; } case 'image_quality': group('output').quality = num(v); break; case 'image_width': (group('output').resize ??= {}).width = num(v); break; case 'image_height': (group('output').resize ??= {}).height = num(v); break; case 'omit_background': group('output').transparent = bool(v); break; case 'full_page': if (bool(v)) group('capture').mode = 'full_page'; break; case 'full_page_scroll': (group('capture').scroll ??= {}).simulate = bool(v); break; case 'full_page_scroll_delay': (group('capture').scroll ??= {}).delay = num(v); break; case 'full_page_max_height': (group('capture').scroll ??= {}).max_height = num(v); break; case 'selector': Object.assign(group('capture'), { mode: 'element', selector: v }); break; case 'error_on_selector_not_found': group('capture').selector_required = bool(v); break; case 'wait_until': { const u = all.at(-1); group('wait').until = u.startsWith('networkidle') ? 'networkidle' : u; if (u === 'networkidle2') warnings.push('networkidle2 mapped to networkidle (0 requests for 500 ms)'); break; } case 'delay': { const ms = Math.round(num(v) * 1000); // seconds -> milliseconds group('wait').delay = Math.min(ms, 10_000); if (ms > 10_000) warnings.push(`delay ${v}s capped at 10 s`); break; } case 'timeout': group('wait').timeout = Math.min(num(v), 60); if (num(v) > 60) warnings.push(`timeout ${v}s clamped to 60 s`); break; case 'wait_for_selector': group('wait').for_selector = v; break; case 'block_ads': group('block').ads = bool(v); break; case 'block_trackers': group('block').trackers = bool(v); break; case 'block_cookie_banners': group('block').cookie_banners = bool(v); break; case 'block_chats': group('block').chat_widgets = bool(v); break; case 'block_requests': group('block').requests = all; break; case 'block_resources': { const ok = all.filter((t) => ['image', 'font', 'media', 'script', 'stylesheet'].includes(t)); if (ok.length) group('block').resources = ok; if (ok.length < all.length) warnings.push(`block_resources: dropped ${all.filter((t) => !ok.includes(t)).join(', ')}`); break; } case 'hide_selectors': group('page').remove = all; break; // ScreenshotOne uses display:none case 'styles': group('page').styles = all; break; case 'scripts': group('page').scripts = all; break; case 'click': group('page').click = v; break; case 'dark_mode': group('browser').dark_mode = bool(v); break; case 'reduced_motion': case 'reduce_motion': group('browser').reduced_motion = bool(v); break; case 'media_type': group('browser').media = v; break; case 'user_agent': group('browser').user_agent = v; break; case 'time_zone': group('browser').timezone = v; break; case 'headers': group('network').headers = Object.fromEntries(all.map((h) => [h.slice(0, h.indexOf(':')).trim(), h.slice(h.indexOf(':') + 1).trim()])); break; case 'cookies': { const host = q.get('url') ? new URL(q.get('url')).hostname : undefined; group('network').cookies = all.map((c) => parseCookie(c, host)); break; } case 'authorization': { const auth = parseAuthorization(v); if (auth) group('network').auth = auth; else group('network').headers = { ...(body.network?.headers ?? {}), Authorization: v }; break; } case 'bypass_csp': group('network').bypass_csp = bool(v); break; case 'cache': if (bool(v)) group('cache').available = true; break; case 'cache_ttl': group('cache').ttl = num(v); break; case 'cache_key': group('cache').key = v; break; case 'response_type': if (v === 'json') warnings.push('response_type=json: send the header Accept: application/json instead'); break; default: warnings.push(`${key} has no RenderScreenshot equivalent; dropped`); } } return { body, warnings }; } if (import.meta.url === `file://${process.argv[1]}`) { const input = process.argv[2] ?? 'url=https://example.com&format=jpg&delay=2&full_page=true&hide_selectors=.promo&hide_selectors=%23chat&block_cookie_banners=true'; console.log(JSON.stringify(fromScreenshotOne(input), null, 2)); }
Run node so-to-rs.mjs 'url=https://example.com&format=jpg&delay=2' to print the converted body.
Python
# so_to_rs.py: translate a ScreenshotOne /take query string into a RenderScreenshot POST body. # Python 3.9+, standard library only. import base64 from urllib.parse import parse_qs, urlparse # ScreenshotOne name -> (RenderScreenshot path, converter) TRUE = ('true', '1') as_bool = lambda v: v in TRUE SIMPLE = { 'viewport_width': ('viewport.width', int), 'viewport_height': ('viewport.height', int), 'viewport_mobile': ('viewport.mobile', as_bool), 'viewport_has_touch': ('viewport.touch', as_bool), 'viewport_landscape': ('viewport.landscape', as_bool), 'image_quality': ('output.quality', int), 'image_width': ('output.resize.width', int), 'image_height': ('output.resize.height', int), 'omit_background': ('output.transparent', as_bool), 'full_page_scroll': ('capture.scroll.simulate', as_bool), 'full_page_scroll_delay': ('capture.scroll.delay', int), 'full_page_max_height': ('capture.scroll.max_height', int), 'error_on_selector_not_found': ('capture.selector_required', as_bool), 'wait_for_selector': ('wait.for_selector', str), 'block_ads': ('block.ads', as_bool), 'block_trackers': ('block.trackers', as_bool), 'block_cookie_banners': ('block.cookie_banners', as_bool), 'block_chats': ('block.chat_widgets', as_bool), 'click': ('page.click', str), 'dark_mode': ('browser.dark_mode', as_bool), 'reduced_motion': ('browser.reduced_motion', as_bool), 'media_type': ('browser.media', str), 'user_agent': ('browser.user_agent', str), 'time_zone': ('browser.timezone', str), 'bypass_csp': ('network.bypass_csp', as_bool), 'cache_ttl': ('cache.ttl', int), 'cache_key': ('cache.key', str), } LISTS = {'block_requests': 'block.requests', 'hide_selectors': 'page.remove', 'styles': 'page.styles', 'scripts': 'page.scripts'} FORMATS = {'png', 'jpeg', 'webp', 'avif', 'pdf', 'html'} RESOURCES = {'image', 'font', 'media', 'script', 'stylesheet'} def put(body, path, value): *parents, leaf = path.split('.') node = body for p in parents: node = node.setdefault(p, {}) node[leaf] = value def parse_cookie(raw, fallback_domain): pair, *attrs = [s.strip() for s in raw.split(';') if s.strip()] name, _, value = pair.partition('=') cookie = {'name': name, 'value': value, 'domain': fallback_domain} for attr in attrs: k, _, v = attr.partition('=') k = k.lower() if k == 'domain': cookie['domain'] = v elif k == 'path': cookie['path'] = v elif k == 'secure': cookie['secure'] = True elif k == 'httponly': cookie['http_only'] = True elif k == 'samesite': cookie['same_site'] = v return cookie def from_screenshotone(query): q = parse_qs(query, keep_blank_values=True) body, warnings = {}, [] for key, values in q.items(): v = values[-1] if key in ('access_key', 'signature'): continue elif key in ('url', 'html', 'markdown'): body[key] = v elif key in SIMPLE: path, conv = SIMPLE[key] put(body, path, conv(v)) elif key in LISTS: put(body, LISTS[key], values) elif key == 'reduce_motion': put(body, 'browser.reduced_motion', as_bool(v)) elif key == 'device_scale_factor': put(body, 'viewport.scale', min(float(v), 3)) if float(v) > 3: warnings.append(f'device_scale_factor {v} clamped to 3') 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} is not supported; using png') elif key == 'full_page': if as_bool(v): put(body, 'capture.mode', 'full_page') elif key == 'selector': put(body, 'capture.mode', 'element'); put(body, 'capture.selector', v) elif key == 'wait_until': put(body, 'wait.until', 'networkidle' if v.startswith('networkidle') else v) if v == 'networkidle2': warnings.append('networkidle2 mapped to networkidle (0 requests for 500 ms)') elif key == 'delay': ms = round(float(v) * 1000) # seconds -> milliseconds put(body, 'wait.delay', min(ms, 10_000)) if ms > 10_000: warnings.append(f'delay {v}s capped at 10 s') elif key == 'timeout': put(body, 'wait.timeout', min(int(v), 60)) if int(v) > 60: warnings.append(f'timeout {v}s clamped to 60 s') elif key == 'block_resources': ok = [t for t in values if t in RESOURCES] if ok: put(body, 'block.resources', ok) if len(ok) < len(values): warnings.append('block_resources: dropped ' + ', '.join(t for t in values if t not in ok)) elif key == 'headers': put(body, 'network.headers', {h.split(':', 1)[0].strip(): h.split(':', 1)[1].strip() for h in values}) elif key == 'cookies': host = urlparse(q['url'][-1]).hostname if 'url' in q else None put(body, 'network.cookies', [parse_cookie(c, host) for c in values]) elif key == 'authorization': scheme, _, token = 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 == 'cache': if as_bool(v): put(body, 'cache.available', True) elif key == 'response_type': if v == 'json': warnings.append('response_type=json: send the header Accept: application/json instead') else: warnings.append(f'{key} has no RenderScreenshot equivalent; dropped') return body, warnings if __name__ == '__main__': import json body, warnings = from_screenshotone( 'url=https://example.com&format=jpg&delay=2&full_page=true' '&hide_selectors=.promo&hide_selectors=%23chat&block_cookie_banners=true&full_page_algorithm=by_sections') print(json.dumps(body, indent=2)) print(warnings)
Ruby
# so_to_rs.rb: translate a ScreenshotOne /take query string into a RenderScreenshot POST body. # Ruby 3.0+, standard library only. require 'base64' require 'uri' module SoToRs BOOL = ->(v) { %w[true 1].include?(v) } SIMPLE = { 'viewport_width' => ['viewport.width', :to_i.to_proc], 'viewport_height' => ['viewport.height', :to_i.to_proc], 'viewport_mobile' => ['viewport.mobile', BOOL], 'viewport_has_touch' => ['viewport.touch', BOOL], 'viewport_landscape' => ['viewport.landscape', BOOL], 'image_quality' => ['output.quality', :to_i.to_proc], 'image_width' => ['output.resize.width', :to_i.to_proc], 'image_height' => ['output.resize.height', :to_i.to_proc], 'omit_background' => ['output.transparent', BOOL], 'full_page_scroll' => ['capture.scroll.simulate', BOOL], 'full_page_scroll_delay' => ['capture.scroll.delay', :to_i.to_proc], 'full_page_max_height' => ['capture.scroll.max_height', :to_i.to_proc], 'error_on_selector_not_found' => ['capture.selector_required', BOOL], 'wait_for_selector' => ['wait.for_selector', :to_s.to_proc], 'block_ads' => ['block.ads', BOOL], 'block_trackers' => ['block.trackers', BOOL], 'block_cookie_banners' => ['block.cookie_banners', BOOL], 'block_chats' => ['block.chat_widgets', BOOL], 'click' => ['page.click', :to_s.to_proc], 'dark_mode' => ['browser.dark_mode', BOOL], 'reduced_motion' => ['browser.reduced_motion', BOOL], 'reduce_motion' => ['browser.reduced_motion', BOOL], 'media_type' => ['browser.media', :to_s.to_proc], 'user_agent' => ['browser.user_agent', :to_s.to_proc], 'time_zone' => ['browser.timezone', :to_s.to_proc], 'bypass_csp' => ['network.bypass_csp', BOOL], 'cache_ttl' => ['cache.ttl', :to_i.to_proc], 'cache_key' => ['cache.key', :to_s.to_proc] }.freeze LISTS = { 'block_requests' => 'block.requests', 'hide_selectors' => 'page.remove', 'styles' => 'page.styles', 'scripts' => 'page.scripts' }.freeze FORMATS = %w[png jpeg webp avif pdf html].freeze module_function def put(body, path, value) *parents, leaf = path.split('.') parents.reduce(body) { |node, key| node[key] ||= {} }[leaf] = value end def parse_cookie(raw, fallback_domain) pair, *attrs = raw.split(';').map(&:strip).reject(&:empty?) name, value = pair.split('=', 2) cookie = { 'name' => name, 'value' => value, 'domain' => fallback_domain } attrs.each do |attr| key, val = attr.split('=', 2) case key.downcase when 'domain' then cookie['domain'] = val when 'path' then cookie['path'] = val when 'secure' then cookie['secure'] = true when 'httponly' then cookie['http_only'] = true end end cookie end def from_screenshotone(query) params = URI.decode_www_form(query).group_by(&:first).transform_values { |pairs| pairs.map(&:last) } body = {} warnings = [] params.each do |key, values| v = values.last case key when 'access_key', 'signature' then next when 'url', 'html', 'markdown' then body[key] = v when *SIMPLE.keys then put(body, SIMPLE[key][0], SIMPLE[key][1].call(v)) when *LISTS.keys then put(body, LISTS[key], values) when 'format' f = v == 'jpg' ? 'jpeg' : v FORMATS.include?(f) ? put(body, 'output.format', f) : warnings << "format #{v} is not supported; using png" when 'full_page' then put(body, 'capture.mode', 'full_page') if BOOL.call(v) when 'selector' put(body, 'capture.mode', 'element') put(body, 'capture.selector', v) when 'device_scale_factor' then put(body, 'viewport.scale', [v.to_f, 3].min) when 'wait_until' then put(body, 'wait.until', v.start_with?('networkidle') ? 'networkidle' : v) when 'delay' ms = (v.to_f * 1000).round # seconds -> milliseconds put(body, 'wait.delay', [ms, 10_000].min) warnings << "delay #{v}s capped at 10 s" if ms > 10_000 when 'timeout' then put(body, 'wait.timeout', [v.to_i, 60].min) when 'headers' put(body, 'network.headers', values.to_h { |h| h.split(':', 2).map(&:strip) }) when 'cookies' host = params['url'] && URI(params['url'].last).host put(body, 'network.cookies', values.map { |c| parse_cookie(c, host) }) when 'authorization' scheme, token = v.split(' ', 2) if scheme.casecmp?('bearer') put(body, 'network.auth', { 'type' => 'bearer', 'token' => token }) elsif scheme.casecmp?('basic') user, password = Base64.strict_decode64(token).split(':', 2) put(body, 'network.auth', { 'type' => 'basic', 'username' => user, 'password' => password }) end when 'block_resources' put(body, 'block.resources', values & %w[image font media script stylesheet]) when 'cache' then put(body, 'cache.available', true) if BOOL.call(v) when 'response_type' warnings << 'response_type=json: send the header Accept: application/json instead' if v == 'json' else warnings << "#{key} has no RenderScreenshot equivalent; dropped" end end [body, warnings] end end if $PROGRAM_NAME == __FILE__ require 'json' body, warnings = SoToRs.from_screenshotone( 'url=https://example.com&format=jpg&delay=2&full_page=true&hide_selectors=.promo&hide_selectors=%23chat&block_cookie_banners=true' ) puts JSON.pretty_generate(body) p warnings end
All three passed the same test vectors: units and selectors, cookies with attributes, a Basic authorization value whose password contains a colon, and four unsupported options that must produce warnings.
Send the converted request
This script replays one logged query string against RenderScreenshot and saves the image. Use it to spot-check a sample before you change application code.
// send.mjs: replay a logged ScreenshotOne query against RenderScreenshot. Node 22+. // Usage: RS_API_KEY=rs_live_... node send.mjs '<screenshotone query string>' out.png import { writeFile } from 'node:fs/promises'; import { fromScreenshotOne } from './so-to-rs.mjs'; const API = process.env.RS_API_BASE ?? 'https://api.renderscreenshot.com'; const [query, out = 'out.png'] = process.argv.slice(2); const { body, warnings } = fromScreenshotOne(query); for (const w of warnings) console.warn(`warning: ${w}`); const res = await fetch(`${API}/v1/screenshot`, { method: 'POST', headers: { Authorization: `Bearer ${process.env.RS_API_KEY}`, 'Content-Type': 'application/json' }, body: JSON.stringify(body), signal: AbortSignal.timeout(65_000), // wait.timeout is 60 s at most }); if (!res.ok) { console.error(`${res.status} ${await res.text()}`); process.exit(1); } await writeFile(out, Buffer.from(await res.arrayBuffer())); console.log(`${out}: ${res.headers.get('x-credits-used')} credit, cache URL ${res.headers.get('x-cache-url')}`);
On a converted request with full_page_algorithm=by_sections, it printed warning: full_page_algorithm has no RenderScreenshot equivalent; dropped, then saved the file. A failing page exits with status 1 and prints the typed error body.
SDK examples
Once a request is converted, the SDKs express it with builder methods. This is the example request from above in each official SDK. Options without a builder method (page scripts, clip, resize) go in the POST body shown earlier.
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) .format('jpeg').quality(80).fullPage().blockCookieBanners().delay(2000) );
Python
import os from renderscreenshot import Client, TakeOptions client = Client(os.environ['RS_API_KEY']) image = client.take( TakeOptions.url('https://example.com').width(1280).height(1024) .format('jpeg').quality(80).full_page().block_cookie_banners().delay(2000) )
Ruby
client = RenderScreenshot.client(ENV.fetch('RS_API_KEY')) image = client.take( RenderScreenshot::TakeOptions.url('https://example.com').width(1280).height(1024) .format('jpeg').quality(80).full_page.block_cookie_banners.delay(2000) )
PHP
$client = new RenderScreenshot\Client(getenv('RS_API_KEY')); $image = $client->take( RenderScreenshot\TakeOptions::url('https://example.com')->width(1280)->height(1024) ->format('jpeg')->quality(80)->fullPage()->blockCookieBanners()->delay(2000) );
Java
Client client = new Client(System.getenv("RS_API_KEY")); byte[] image = client.take( TakeOptions.url("https://example.com").width(1280).height(1024) .format("jpeg").quality(80).fullPage().blockCookieBanners().delay(2000) );
Go
client, err := rs.New(os.Getenv("RS_API_KEY")) if err != nil { log.Fatal(err) } image, err := client.Take(ctx, rs.URL("https://example.com").Width(1280).Height(1024). Format(rs.FormatJPEG).Quality(80).FullPage().BlockCookieBanners().Delay(2000))
The Node.js, Python, Ruby, PHP and Go chains ran against the mock API; the Java example follows the Java SDK reference and was not run.
Responses and errors
- Image bytes come back by default on both sides. Our metadata is in
X-Screenshot-*headers, plusX-Cache-URL(a public CDN link),X-Cache-Key,X-Credits-UsedandX-Credits-Remaining. See response headers. - Errors are JSON with
error.type,error.codeand aretryableboolean. Retry whenretryableis true; fix the request when it is false. The codes are listed in error codes. - Credits. A fresh render costs 1 credit, a failed render costs nothing, and a
cache.availablehit costs nothing. Details in credits and billing.
Cutover checklist
- Convert every distinct query string and resolve each warning: accept the loss, or replace the option (for example
page.scriptsinstead offull_page_algorithm). - Set explicit
output.formatandviewport.heightwherever you relied on ScreenshotOne defaults. - Move the key out of URLs: Bearer header on the server, signed URLs for
<img>andog:imagetags. - Replace
async+webhook_urlwith a dashboard webhook or a batch job. - Render a sample of your real URLs on both services and compare the images by eye. The shadow-mode script automates this.
- Switch traffic, keep the ScreenshotOne key for a week, then revoke it.
Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| Captures happen 1,000 times too late, or time out | delay copied in seconds as milliseconds, or the reverse |
Multiply ScreenshotOne seconds by 1,000; stay under 10,000 ms |
| PNG files where you expected JPEG | Relied on ScreenshotOne's jpg default |
Set output.format: "jpeg" |
| Images are shorter than before | Default height 800 instead of 1024 | Set viewport.height: 1024 |
| Hidden elements leave blank gaps | Used page.hide for hide_selectors |
Use page.remove, which collapses the space like display: none |
validation_error on cookies |
Cookie sent as a string or without domain |
Send objects with name, value and domain |
401 authentication_error |
access_key left in the query |
Send Authorization: Bearer rs_live_... |
| Logged-in page shows the login screen | authorization header copied into network.headers but the site uses a session cookie |
Send network.cookies, see authenticated pages |
| Webhook never arrives | webhook_url dropped by the converter |
Set the webhook URL in Dashboard, Webhooks |
| Retina images are smaller | device_scale_factor 4 or 5 clamped to 3 |
Accept 3, or resize with output.resize |
Limits to know
viewport.width 1 to 7,680 px, viewport.height 1 to 4,320 px, viewport.scale 1 to 3, wait.timeout up to 60 s, wait.delay up to 10,000 ms, capture.scroll.max_height default 16,000 px, cache.ttl 3,600 to 2,592,000 s, signed URLs valid for 30 days at most. Batch sizes and rate limits per plan are in rate limits.
Related
- ScreenshotOne alternative: features, prices and a shadow-mode test.
- Migrate from Urlbox and migrate from Puppeteer.
- POST /v1/screenshot: every parameter group.
- Ready to convert your first request? Create a free API key.