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:

  1. Search your code and logs for api.screenshotone.com/take.
  2. Copy each distinct query string (without access_key) into a file, one per line.
  3. 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, plus X-Cache-URL (a public CDN link), X-Cache-Key, X-Credits-Used and X-Credits-Remaining. See response headers.
  • Errors are JSON with error.type, error.code and a retryable boolean. Retry when retryable is 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.available hit costs nothing. Details in credits and billing.

Cutover checklist

  1. Convert every distinct query string and resolve each warning: accept the loss, or replace the option (for example page.scripts instead of full_page_algorithm).
  2. Set explicit output.format and viewport.height wherever you relied on ScreenshotOne defaults.
  3. Move the key out of URLs: Bearer header on the server, signed URLs for <img> and og:image tags.
  4. Replace async + webhook_url with a dashboard webhook or a batch job.
  5. Render a sample of your real URLs on both services and compare the images by eye. The shadow-mode script automates this.
  6. 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 in this topic

Was this page helpful?