ScreenshotNeo

BlogHow-to

Ruby Screenshot API: Capture Any Website in Code

Capture websites from Ruby with hosted APIs or Ferrum, including full-page, selector, authenticated, reliable and production-ready examples.

By the ScreenshotNeo team29 September 20269 min read

Ruby Screenshot API: Capture Any Website in Code

A Ruby screenshot API lets your application send a URL and receive rendered pixels without installing Chrome. You can also run Chrome yourself with Ferrum when you need browser-level control. For most production jobs, choose a hosted API when you want predictable deployment and concurrency; choose Ferrum when pages are private, browser state must stay inside your network, or you need custom DevTools operations.

Quick decision

Need Best fit Reason
One HTTP call, no browser fleet Hosted API The provider owns Chrome, patching, queues and scaling.
Private pages behind your firewall Ferrum Your process can reach internal hosts and keep credentials local.
Full-page, selector, device and wait controls Either Hosted APIs expose these as request options; Ferrum exposes the browser directly.
High parallel volume Hosted API or a managed Ferrum pool Each Chrome process consumes memory; a service can enforce concurrency.

Hosted products differ in endpoint names, retention, quotas and pricing. Confirm those details in provider documentation before shipping. The examples below keep credentials on the server and write binary responses directly to disk or object storage.

Option 1: call a hosted screenshot API from Ruby

The common shape is a JSON POST containing a target URL and capture options, authenticated with an API key. RenderKit documents PNG, JPEG and WebP output, full-page and selector capture, ad/cookie blocking, device scale and wait controls. html2img documents public URL capture with viewport, full-page, selector, CSS injection and delayed-content options. Screenshot API documents GET and POST endpoints, API-key authentication, PNG/JPEG/WebP/PDF output and advanced POST options. Their exact parameter names are provider-specific.

A screenshot pipeline: request, render, and binary output.
A screenshot pipeline: request, render, and binary output.

Runnable Net::HTTP client

require 'net/http'
require 'json'
require 'uri'

endpoint = URI(ENV.fetch('SCREENSHOT_ENDPOINT'))
api_key  = ENV.fetch('SCREENSHOT_API_KEY')

payload = {
  url: 'https://example.com',
  format: 'webp',
  full_page: true,
  viewport: { width: 1440, height: 900 },
  device_scale_factor: 2,
  wait: { selector: '#main', timeout_ms: 10_000 }
}

request = Net::HTTP::Post.new(endpoint)
request['Authorization'] = "Bearer #{api_key}"
request['Content-Type'] = 'application/json'
request['Accept'] = 'image/webp'
request.body = JSON.generate(payload)

http = Net::HTTP.new(endpoint.host, endpoint.port)
http.use_ssl = endpoint.scheme == 'https'
http.open_timeout = 10
http.read_timeout = 90

response = http.start { |connection| connection.request(request) }
unless response.is_a?(Net::HTTPSuccess)
  warn "capture failed (#{response.code}): #{response.body}"
  exit 1
end

File.binwrite('shot.webp', response.body)
puts "saved #{response.body.bytesize} bytes"

Set SCREENSHOT_ENDPOINT and SCREENSHOT_API_KEY in your deployment secret store. Do not put keys in client-side JavaScript, URLs logged by proxies, or checked-in Rails credentials. Some APIs return an image immediately; others return JSON containing a download URL. Branch on Content-Type and parse JSON when the response is not an image.

POST options to model explicitly

Option Use Watch for
url Public page to render. Redirects, robots rules and geo restrictions can change the result.
format png for lossless UI, jpeg for small photos, webp for a compact modern format; some providers also offer PDF. JPEG has no alpha channel. Verify PDF page sizing separately.
viewport Set CSS pixel width and height for responsive layouts. Mobile breakpoints depend on width, user agent and touch emulation.
full_page Capture document height instead of the viewport. Lazy images may need a delay or scroll-triggered loading; very tall pages create large files.
selector Capture one CSS element. Wait until the element exists and has non-zero dimensions.
wait Delay, selector wait or network-idle wait for client-rendered content. Network idle can never occur on pages with analytics or streams; use a bounded delay or selector timeout.
CSS injection Hide animations, force print styles, or adjust layout for a report. Keep overrides deterministic and versioned with your code.
Blocking Block ads, cookies, trackers or resource types to reduce noise and bytes. Blocking a required script can leave a blank component.

Option 2: self-host Chrome with Ferrum

Ferrum is a Ruby API over the Chrome DevTools Protocol. A Chrome or Chromium binary must be installed where the Ruby process runs, and you own browser lifecycle, updates, memory limits and concurrency.

Minimal screenshot script

require 'ferrum'

browser = Ferrum::Browser.new(
  browser_path: ENV['CHROME_BIN'],
  window_size: [1440, 900],
  timeout: 30
)

begin
  browser.go_to('https://example.com')
  browser.network.wait_for_idle
  browser.screenshot(path: 'example.png', full: true)
ensure
  browser.quit
end

Install the gem with gem install ferrum or add gem 'ferrum' to your Gemfile. In containers, install a compatible Chrome/Chromium package and its shared libraries, set CHROME_BIN, and run as a user permitted to start the sandbox. Keep one browser per job or maintain a bounded pool; creating unlimited instances will exhaust RAM and file descriptors.

Element capture, JavaScript and authentication

require 'ferrum'

browser = Ferrum::Browser.new(window_size: [1280, 800], timeout: 45)
begin
  browser.go_to('https://example.com/dashboard')
  browser.cookies.set(name: 'session', value: ENV.fetch('SESSION_COOKIE'), domain: 'example.com')
  browser.go_to('https://example.com/dashboard')
  browser.at_css('#revenue-chart').wait_until(&:present?)
  browser.evaluate <<~JS
    document.querySelectorAll('.cookie-banner, .chat-widget').forEach { |el| el.remove() }
  JS
  browser.at_css('#revenue-chart').screenshot(path: 'chart.png')
ensure
  browser.quit
end

Ferrum also lets you execute JavaScript, set cookies and inspect the DOM. Treat those capabilities as application code: validate selectors, avoid embedding secrets in page scripts, and clear state between jobs if a browser is reused.

Rails integration pattern

Wrap capture in a service object and enqueue it with Active Job. Return an Active Storage blob or object-storage key instead of holding a multi-megabyte image in a web request.

class CapturePageJob < ApplicationJob
  queue_as :screenshots

  def perform(url, output_key)
    response = ScreenshotClient.capture(url)
    raise "capture failed: #{response.status}" unless response.success?

    ScreenshotBlobStore.write(output_key, response.body, content_type: response.content_type)
  end
end

Validate allowed URL schemes and hosts before fetching user-supplied URLs. If your service accepts arbitrary URLs, enforce egress rules to protect internal metadata endpoints and private network services.

Dynamic pages and edge cases

  • Cookie or consent dialogs: wait for the page, then dismiss the dialog or inject CSS that hides it. A hosted provider may offer consent handling; verify whether it clicks, removes, or only hides the banner.
  • Lazy loading: use full-page mode plus a delay, or scroll in Ferrum before capture so images enter the viewport.
  • Animations: disable transitions with injected CSS or wait for a stable selector. Otherwise two captures can differ by a frame.
  • Fonts: wait for document.fonts.ready; missing web fonts cause layout shifts and fallback glyphs.
  • Cross-origin frames: you may capture the outer page but cannot read another origin’s DOM from page JavaScript.
  • Very tall documents: split into sections or use PDF pagination. Browser bitmap limits vary by Chromium version and host memory.
  • Authenticated pages: send headers/cookies only through a provider that documents them, or use Ferrum with an isolated context. Never log authorization headers.
  • Bot checks and CAPTCHAs: detect challenge pages and report a distinct failure rather than storing them as valid screenshots.
  • Redirects and certificates: allow expected redirects, and fix certificate chains instead of disabling TLS verification in production.

Reliability, performance and cost

Make captures repeatable

  1. Pin viewport, device scale, locale, timezone and user agent.
  2. Wait on a semantic selector such as a chart container, not an arbitrary long sleep.
  3. Record URL, options, response status, render duration and final dimensions.
  4. Retry transient network failures with exponential backoff and a finite attempt count. Do not retry deterministic 4xx errors.
  5. Hash normalized options and URL to deduplicate identical work; cache only when freshness permits.

Control resource use

Viewport screenshots are cheaper to transfer than full-page images. WebP usually reduces bytes for photographic pages; PNG preserves sharp text and transparency. With Ferrum, measure resident memory per browser under your own pages, cap concurrent jobs, recycle browsers after a bounded number of captures, and enforce a total navigation timeout. Hosted APIs shift those browser costs into per-request pricing and quotas; compare included volume, overage behavior, retention and cache policy before selecting a plan.

Security checklist

  • Keep API keys and session cookies in server-side secret storage.
  • Allow-list outbound domains when users can submit URLs.
  • Strip sensitive response headers from logs.
  • Use separate browser profiles or contexts per tenant.
  • Set maximum image dimensions and response sizes to prevent memory exhaustion.

Troubleshooting

Symptom Likely cause Fix
401/403 from API Missing, expired or incorrectly formatted key. Check the provider’s required header/query format and rotate the secret.
Blank or white image JavaScript not finished, blocked asset, bot challenge or zero-size selector. Wait for a selector, inspect final HTML, relax blocking and classify challenge pages.
Missing lower-page images Lazy loading triggered only by scroll. Use provider full-page lazy-load support or scroll the document in Ferrum.
Timeout Slow origin, never-ending network activity or an unreachable host. Set navigation and read timeouts, wait on a bounded selector, and retry transient failures.
Ferrum cannot start Chrome Binary absent, incompatible version, sandbox or shared-library issue. Install matching Chromium dependencies, set browser_path, and inspect container permissions.
Fonts or layout differ Web fonts not loaded, wrong viewport, locale or device scale. Pin those values and wait for document.fonts.ready.
File is huge Full-page capture, high device scale or uncompressed PNG. Capture a selector, lower scale, choose WebP/JPEG, or paginate as PDF.

Or skip the browser setup

ScreenshotNeo is a hosted website screenshot API and MCP server. One GET request returns PNG, JPEG, WebP or PDF. The service accepts consent banners like a visitor and removes more than 60 known consent platforms, newsletter popups and chat widgets before capture; each step can be turned off. Only clean shots are billed: bot checks, CAPTCHAs, blank pages, timeouts, failed loads and cache hits cost nothing, and X-Page-Verdict and X-Billed headers explain the result.

Ruby can call the API with the standard library:

require 'net/http'
require 'uri'

uri = URI('https://api.screenshotneo.com/v1/shot')
uri.query = URI.encode_www_form(access_key: ENV.fetch('SCREENSHOTNEO_KEY'), url: 'https://stripe.com')
response = Net::HTTP.get_response(uri)
raise "capture failed: #{response.code}" unless response.is_a?(Net::HTTPSuccess)
File.binwrite('shot.webp', response.body)

See the ScreenshotNeo API documentation for all options. The same endpoint supports full-page and CSS-selector captures, dark mode, 12 device presets or any viewport, retina scale, PDF paper size/margins/landscape/page ranges, HTML/CSS input, custom CSS and JavaScript, clicks, waits, blocking rules, headers, cookies, user agent, Authorization, timezone, geolocation, transparent backgrounds, resizing, TTL caching, signed links, async webhooks, bulk capture of 100 URLs and a usage API. Its MCP server exposes take_screenshot, get_page_info and capture_pdf to Claude, Cursor and other MCP clients.

cURL

curl -G 'https://api.screenshotneo.com/v1/shot' -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp

Python

import requests
r = requests.get('https://api.screenshotneo.com/v1/shot', params={'access_key': 'YOUR_API_KEY', 'url': 'https://stripe.com'}, timeout=90)
open('shot.webp', 'wb').write(r.content)

Node.js

const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);

The Free plan includes 1,000 shots each month with no card. Paid plans start at $5 for 3,000 shots; yearly billing gives two months free. Create a free ScreenshotNeo account to get an API key.

FAQ

Can Ruby capture a single CSS selector?

Yes. Hosted APIs expose a selector option; Ferrum can locate the element and call its screenshot method. Wait until it is present and has dimensions.

Consent banners and overlays can be handled before capture.
Consent banners and overlays can be handled before capture.

Is Ferrum a Ruby gem or a separate service?

Ferrum is a Ruby library that drives a local Chrome or Chromium process. Your deployment supplies and maintains the browser binary.

Should I use a delay or network-idle wait?

Use a selector wait when possible. Network idle is useful for finite pages but unreliable with analytics, polling and streaming connections. A bounded delay is a fallback.

How do I capture private pages?

Use Ferrum inside the trusted network, or a hosted API that explicitly supports headers, cookies or authenticated browser contexts. Keep credentials out of logs.

Which image format should I return?

PNG is best for crisp text and transparency, JPEG for photographic content without alpha, and WebP when clients support it and transfer size matters.