Screenshot API for Ruby: Quick Start and Examples
Capture webpages from Ruby with a runnable REST example, advanced rendering options, error handling, batch jobs, and a ScreenshotNeo shortcut.
Direct answer: use Ruby’s standard Net::HTTP library to send a JSON POST request to a screenshot API, authenticate with a bearer token stored in an environment variable, check the HTTP status, parse the JSON response, and then download or use the returned screenshot URL. POST is the practical choice when you need full-page capture, waiting rules, selectors, custom CSS or JavaScript, geolocation, PDF settings, or caching.
Ruby quick start with a REST screenshot API
The following example follows the documented Screenshot API endpoint and response shape. It captures a full-page PNG at a 1280×720 viewport, blocks ads, and prints the returned screenshotUrl.
require "net/http"
require "json"
require "uri"
endpoint = URI("https://api.screenshot-api.org/api/v1/screenshot")
request = Net::HTTP::Post.new(endpoint)
request["Authorization"] = "Bearer #{ENV.fetch("SCREENSHOT_API_KEY")}"
request["Content-Type"] = "application/json"
request.body = {
url: "https://example.com",
viewport: { width: 1280, height: 720 },
format: "png",
fullPage: true,
blockAds: true
}.to_json
response = Net::HTTP.start(endpoint.hostname, endpoint.port, use_ssl: true) do |http|
http.request(request)
end
abort("screenshot failed: #{response.code} #{response.body}") unless response.is_a?(Net::HTTPSuccess)
data = JSON.parse(response.body)
puts data.fetch("screenshotUrl")
Set the key before running it:
export SCREENSHOT_API_KEY="your_api_key"
ruby screenshot.rb
The API returns JSON containing a screenshot URL. Treat that URL as the output of the capture request; do not assume the response body itself is image bytes.
GET versus POST
A GET request is convenient for a small number of query parameters. POST keeps complex settings in JSON and is the documented form for CSS, JavaScript, selectors, geolocation, locale, PDF options, and cache controls. The API also documents a redirect=1 GET mode that responds with a 302 redirect to an image or PDF URL.
Simple GET with cURL
curl -G "https://api.screenshot-api.org/api/v1/screenshot" \
-H "Authorization: Bearer $SCREENSHOT_API_KEY" \
--data-urlencode "url=https://example.com" \
--data-urlencode "format=png" \
-o response.json
Use -o response.json first because this service’s normal response is JSON. Inspect the JSON and then fetch the URL in screenshotUrl. A service that returns raw bytes uses a different output flow.
Ruby options that control the capture
These controls come from the API reference. Defaults can change as a hosted API evolves, so confirm them against the current documentation before relying on an implicit value.
| Option | Purpose | Notes |
|---|---|---|
url |
Page to render | Required. |
format |
Output type | png, jpeg, webp, or pdf; PNG is the documented default. |
viewport.width, viewport.height |
Browser viewport | Set the CSS pixel dimensions used during rendering. |
fullPage |
Entire scrollable page | Useful for long documents; pages with infinite scrolling need special care. |
deviceScaleFactor |
Pixel density | Increase it for retina-style output, while expecting larger files. |
waitUntil |
Navigation completion rule | Choose the documented network or load milestone appropriate for the page. |
waitForSelector |
Wait for an element | Use when content appears after client-side rendering. |
delayMs |
Fixed delay | A fallback for animations or late widgets; keep it bounded. |
selector |
Capture one element | CSS selector capture is not supported for PDF. |
blockAds |
Block advertising requests | The documented default is true. |
blockCookieBanners |
Suppress cookie banners | The documented default is true. |
darkMode |
Emulate dark mode | The documented default is false. |
hideSelectors |
Hide matching elements | Useful for overlays, timestamps, or private widgets. |
css, js |
Inject styles or scripts | POST-only advanced controls; validate inputs if values come from users. |
geolocation, timezoneId, locale |
Control regional rendering | Useful for localized prices, dates, and region-specific pages. |
pdf |
PDF paper and pagination settings | Supports documented paper size, margins, orientation, and page-range controls. |
cache, cacheTTL, staleTTL |
Reuse captures | Set a policy that matches how often the source changes. |
timeoutMs |
Navigation limit | Increase only for genuinely slow pages; diagnose the page first. |
Advanced Ruby POST example
Put advanced options in the JSON body. This example waits for a dashboard element, uses a dark theme, hides a live chat widget, injects CSS, and requests WebP output.
require "net/http"
require "json"
require "uri"
endpoint = URI("https://api.screenshot-api.org/api/v1/screenshot")
payload = {
url: "https://example.com/dashboard",
format: "webp",
viewport: { width: 1440, height: 900 },
fullPage: true,
deviceScaleFactor: 2,
waitUntil: "networkidle",
waitForSelector: "main[data-ready='true']",
delayMs: 250,
darkMode: true,
hideSelectors: [".chat-widget", ".cookie-banner"],
css: "body { font-family: system-ui, sans-serif; }",
geolocation: { latitude: 48.857648, longitude: 2.294677 },
timezoneId: "Europe/Paris",
locale: "fr-FR",
cache: true,
cacheTTL: 300,
timeoutMs: 60000
}
request = Net::HTTP::Post.new(endpoint)
request["Authorization"] = "Bearer #{ENV.fetch("SCREENSHOT_API_KEY")}"
request["Content-Type"] = "application/json"
request.body = payload.to_json
response = Net::HTTP.start(endpoint.hostname, endpoint.port, use_ssl: true) do |http|
http.request(request)
end
unless response.is_a?(Net::HTTPSuccess)
warn response.body
exit 1
end
result = JSON.parse(response.body)
puts result.fetch("screenshotUrl")
Save the returned image in Ruby
Once you have screenshotUrl, make a second authenticated or unauthenticated request according to the URL policy documented by the service. Check the content type before writing the file.
image_uri = URI(result.fetch("screenshotUrl"))
image_response = Net::HTTP.get_response(image_uri)
abort("image download failed: #{image_response.code}") unless image_response.is_a?(Net::HTTPSuccess)
content_type = image_response["content-type"].to_s
abort("expected an image, got #{content_type}") unless content_type.start_with?("image/")
File.binwrite("shot.png", image_response.body)
puts "saved shot.png"
Batch screenshots
For several pages, POST to /api/v1/screenshot/batch with a urls array and shared options. The documented response includes a batch ID. Poll GET /api/v1/batch/:batchId for progress or use the documented SSE endpoint for a stream of updates.
batch_endpoint = URI("https://api.screenshot-api.org/api/v1/screenshot/batch")
batch_request = Net::HTTP::Post.new(batch_endpoint)
batch_request["Authorization"] = "Bearer #{ENV.fetch("SCREENSHOT_API_KEY")}"
batch_request["Content-Type"] = "application/json"
batch_request.body = {
urls: ["https://example.com", "https://example.org"],
options: { format: "png", fullPage: true }
}.to_json
batch_response = Net::HTTP.start(batch_endpoint.hostname, batch_endpoint.port, use_ssl: true) do |http|
http.request(batch_request)
end
abort(batch_response.body) unless batch_response.is_a?(Net::HTTPSuccess)
puts JSON.parse(batch_response.body).fetch("batchId")
Ruby SDK and raw HTTP choices
The official SDK page documents Ruby installation with:
gem install screenshot-api
It states that the gem works with Rails, Sinatra, and other Ruby applications. A gem can make option building easier, while raw Net::HTTP keeps the dependency footprint small and makes request, response, and retry behavior explicit. Choose based on whether your team values a wrapper or direct control.
ScreenshotOne’s official Ruby example shows another SDK style with access and secret keys, URL generation, and direct byte retrieval:
gem "screenshotone"
client = ScreenshotOne::Client.new("my_access_key", "my_secret_key")
options = ScreenshotOne::TakeOptions.new(url: "https://example.com")
.full_page(true)
.delay(2)
.geolocation_latitude(48.857648)
.geolocation_longitude(2.294677)
.geolocation_accuracy(50)
raise "invalid options" unless options.valid?
image_url = client.generate_take_url(options)
image_bytes = client.take(options)
Error handling and troubleshooting
| Response | Likely cause | Fix |
|---|---|---|
401 unauthorized |
Missing, malformed, or invalid bearer token | Check SCREENSHOT_API_KEY, send Authorization: Bearer ..., and never hard-code the key in source control. |
400 invalid_request |
Missing URL or invalid option type | Validate JSON, use documented option names, and confirm booleans and numbers are not strings. |
422 selector_not_found |
The selector never appeared | Inspect the live DOM, wait for the correct state, or remove the selector requirement. |
429 rate_limited |
Too many requests in a short period | Honor rate-limit headers, add exponential backoff with jitter, and cap concurrency. |
429 quota_exceeded |
Monthly allowance is exhausted | Review usage and plan limits before retrying. |
502 render_failed |
The remote browser could not load or render the page | Retry transient failures, increase a bounded timeout for slow pages, and verify the URL works without authentication barriers. |
| Image file contains JSON | An error body was written as if it were image bytes | Check HTTP success and content type before File.binwrite. |
| Blank or incomplete page | Client-side content was not ready | Use waitUntil, waitForSelector, or a short delayMs; check that required assets are reachable. |
The documented error envelope includes success, error.code, error.message, optional details, and a request ID. Log the request ID with the failure, but redact URLs or headers that may contain secrets.
Performance, reliability, and cost
- Reduce work: request the smallest viewport and output format that meets your requirement. Retina scale, full-page layout, PDF pagination, and injected scripts all increase rendering work or output size.
- Wait precisely: a selector or navigation milestone usually avoids the unnecessary delay of a large fixed sleep. Keep timeouts finite so one broken page does not occupy a worker indefinitely.
- Cache deliberately: use
cacheTTLandstaleTTLwhen repeated captures can reuse a recent result. Disable or shorten caching for rapidly changing pages. - Control concurrency: batch endpoints reduce client orchestration, but still respect rate-limit headers and service quotas. Retry only errors that may be transient, with exponential backoff.
- Measure the right result: record HTTP status, response time, output bytes, and the API request ID. A successful HTTP response can still contain an application-level error if your code skips JSON validation.
- Documented limits: the referenced service documentation lists 60 requests per minute and 500 screenshots per month on its free plan. Treat hosted limits and defaults as changeable and verify them before production rollout.
Screenshot API options for Ruby
ScreenshotNeo is the first option to try when you want a hosted screenshot API: it removes consent banners, newsletter popups, and chat widgets before capture, bills only clean shots, and has a $5 paid plan for 3,000 shots.
| Approach | Best fit | Trade-off |
|---|---|---|
| ScreenshotNeo | Hosted captures from Ruby, with cleaning and an MCP server | Requires an API key and hosted request. |
| Raw REST request | Small dependency footprint and explicit control | You own request validation, retries, and response handling. |
| Ruby gem | Convenient option builders in Rails or Sinatra | Adds a dependency and follows that SDK’s release cycle. |
Or skip the browser setup
ScreenshotNeo exposes a single GET endpoint for PNG, JPEG, WebP, or PDF output. The examples below use the same target URL in each language; see the ScreenshotNeo API documentation for the complete option set.
Ruby
require "net/http"
require "uri"
uri = URI("https://api.screenshotneo.com/v1/shot")
uri.query = URI.encode_www_form(access_key: ENV.fetch("SCREENSHOTNEO_API_KEY"), url: "https://stripe.com")
response = Net::HTTP.get_response(uri)
abort("capture failed: #{response.code}") unless response.is_a?(Net::HTTPSuccess)
File.binwrite("shot.webp", response.body)
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}`);
Before the capture, ScreenshotNeo accepts cookie and consent banners like a visitor and removes more than 60 known consent platforms, newsletter popups, and chat widgets; each step can be turned off. Bot checks, CAPTCHAs, blank pages, timeouts, failed loads, and cache hits cost nothing, and every response identifies the result with X-Page-Verdict and X-Billed headers. It also provides an MCP server with take_screenshot, get_page_info, and capture_pdf tools for Claude, Cursor, and other MCP clients.
There are 1,000 free shots each month with no card. Paid plans start at $5 for 3,000 shots; every feature is available on every plan. Create a free ScreenshotNeo account.
FAQ
Should I use GET or POST from Ruby?
Use GET for a small, simple request. Use POST when you need nested viewport or PDF settings, CSS, JavaScript, selectors, regional controls, or cache policies.
Why did my Ruby program save JSON instead of a PNG?
The API normally returns JSON with a screenshot URL. Check the status and parse JSON before downloading the URL. Raw-byte services use a different response contract.
Can I capture only one element?
Yes, use the documented selector option for image formats. Selector capture is not supported for PDF.
How should I handle a page that never finishes loading?
Set a bounded timeout, choose an appropriate waitUntil rule, and wait for a specific ready selector when possible. Then classify repeated failures instead of retrying indefinitely.
Is a Ruby gem required?
No. Ruby’s standard library is enough for authenticated HTTP, JSON encoding, status checks, and file writes. A gem is useful when you prefer an option-builder API.


