How to Capture a Website Screenshot with a Screenshot API in Ruby on Rails
Capture remote webpages from Rails with a screenshot API. Learn to configure credentials, request and store image bytes, handle failures, and move slow captures to Active Job.
To capture a screenshot of an arbitrary website from a Ruby on Rails app, make a server-side HTTPS request to a screenshot API with the target URL and capture options, then validate and handle the response as image bytes. Keep the API key in Rails server configuration. For captures that should not hold up a web request, enqueue the work with Active Job.
This is different from Rails system-test screenshots: Rails’ screenshot helper captures the app in a test browser for debugging; it is not a general API for rendering arbitrary remote URLs. See the Rails testing guide.
1. Choose the request and output shape
A typical flow is Rails server → screenshot provider over HTTPS → provider renders the remote page → provider returns image bytes or another configured response → Rails stores or uses the result. Before choosing a provider or writing a client, decide:
- Whether you need a viewport screenshot or the full page.
- The viewport width and height, and whether the output should be PNG, JPEG, or WebP.
- How the page should be considered ready, such as a provider-supported wait condition or delay.
- Whether the provider returns bytes directly or a URL/storage reference.
- What the feature should display or retry when the target page or provider request fails.
Provider option names and response behavior are provider-specific. ScreenshotOne documents HTTPS GET and POST requests, authentication options, binary image responses, and JSON error responses. Its getting started guide and options reference describe its interface. Urlbox also publishes a Ruby example with its own signing and capture options. Do not assume one provider’s parameters can be sent unchanged to another.
2. Configure Rails credentials
Store the provider key in server-side configuration, such as Rails credentials, an environment variable, or your deployment platform’s secret store. Do not put it in browser JavaScript, a public HTML attribute, or a client-visible URL. Use HTTPS: ScreenshotOne specifically cautions that HTTP can expose keys and other sensitive data in transit.
For example, add a secret using Rails credentials and read it in the service object:
bin/rails credentials:edit
# config/credentials.yml.enc
screenshot_provider:
access_key: YOUR_PROVIDER_API_KEY
The placeholder is not a real credential. Replace it with a key from the provider you choose. If you use environment variables instead, configure the variable in the server environment and read it with ENV.fetch.
3. Build a small Ruby client
This example uses Ruby’s standard Net::HTTP library and illustrates the request and response checks. Replace the endpoint, authentication header, and option names with those documented by your provider. ScreenshotOne’s Ruby screenshot API page includes its Ruby-specific approach; its documentation supports a key in an X-Access-Key header, among other documented authentication methods.
# app/services/website_screenshot.rb
require "net/http"
require "uri"
class WebsiteScreenshot
class Error < StandardError; end
class ProviderError < Error; end
ENDPOINT = URI("https://api.screenshotone.com/take") # Example provider endpoint
def initialize(access_key: Rails.application.credentials.dig(:screenshot_provider, :access_key))
raise Error, "Missing screenshot provider access key" if access_key.blank?
@access_key = access_key
end
# Returns the raw image bytes. Callers can save these with Active Storage,
# write them to a file, or pass them to another image-processing step.
def call(url:, full_page: false, width: 1440, height: 900, format: "png")
uri = ENDPOINT.dup
uri.query = URI.encode_www_form(
url: url,
full_page: full_page,
viewport_width: width,
viewport_height: height,
format: format
)
request = Net::HTTP::Get.new(uri)
request["X-Access-Key"] = @access_key
response = Net::HTTP.start(
uri.host,
uri.port,
use_ssl: true,
open_timeout: 10,
read_timeout: 90
) { |http| http.request(request) }
unless response.is_a?(Net::HTTPSuccess)
# Providers may return JSON with an error code and message. Keep the
# status and a bounded body excerpt for diagnostics; do not save it as an image.
raise ProviderError, "Screenshot provider returned HTTP #{response.code}: #{response.body.to_s.byteslice(0, 1000)}"
end
content_type = response["content-type"].to_s.split(";").first
unless %w[image/png image/jpeg image/webp].include?(content_type)
raise ProviderError, "Expected an image response, received #{content_type.presence || 'no Content-Type'}"
end
response.body
end
end
Important: The endpoint and option names above are illustrative for the client pattern, not a claim that every provider uses that exact endpoint or those exact parameters. Use the selected provider’s published Ruby example or API reference to set them. If the provider supports POST JSON, use that for options when appropriate; ScreenshotOne documents a 100 MiB request-body maximum and advises hosting large HTML or Markdown inputs and referencing them by URL instead of putting them in a query string. A normal URL screenshot needs only the URL and the options the feature requires.
Saving a successful image
If the app has Active Storage configured, attach the returned bytes with an explicit content type and filename. Verify the response first, as the service does above.
bytes = WebsiteScreenshot.new.call(
url: "https://example.com",
full_page: true,
width: 1440,
height: 1000,
format: "webp"
)
record.screenshot.attach(
io: StringIO.new(bytes),
filename: "website-screenshot.webp",
content_type: "image/webp"
)
For a filesystem write instead, use binary mode so the image bytes are not altered:
File.binwrite("tmp/website-screenshot.png", bytes)
4. Use Active Job for work outside the request cycle
Remote rendering can take long enough that the user-facing request should not wait for it. Rails Active Job provides a common interface for background jobs, and perform_later enqueues work. See Active Job Basics and the ActiveJob API.
# app/jobs/capture_website_screenshot_job.rb
class CaptureWebsiteScreenshotJob < ApplicationJob
queue_as :default
retry_on WebsiteScreenshot::ProviderError, wait: :polynomially_longer, attempts: 3
def perform(record_id, url)
record = ScreenshotRecord.find(record_id)
bytes = WebsiteScreenshot.new.call(url: url, full_page: true, format: "png")
record.image.attach(
io: StringIO.new(bytes),
filename: "screenshot-#{record.id}.png",
content_type: "image/png"
)
record.update!(status: "complete")
rescue WebsiteScreenshot::Error => error
record&.update(status: "failed", error_message: error.message)
raise
end
end
# Controller or other application code
record = ScreenshotRecord.create!(status: "queued")
CaptureWebsiteScreenshotJob.perform_later(record.id, "https://example.com")
Adjust retry policy to the provider’s error semantics. A permanent invalid URL or authentication error should not be retried indefinitely; transient network failures may merit a limited retry. Persist an explicit state so clients can distinguish queued, complete, and failed captures. The provider may return bytes or a reference depending on configuration, so adapt persistence to the documented response.
5. Make the target URL safe for your feature
If users choose the target URL, define which destinations your feature permits and validate input against that scope. Consider the risk created when a remote rendering service is asked to visit arbitrary destinations. The reviewed Rails and provider references do not establish a universal Rails-specific SSRF policy or a provider-side isolation guarantee, so do not assume the screenshot service removes this design responsibility. Restrict accepted schemes and destinations according to your application’s use case, and avoid turning an unrestricted screenshot endpoint into a proxy for arbitrary user-supplied URLs.
6. Tune capture settings intentionally
| Choice | When it matters | What to check |
|---|---|---|
| Viewport or full page | Dashboards and previews often need a fixed viewport; archival or review workflows may need the full document. | Provider option name and how very long pages are represented. |
| Width and height | Responsive layouts change with viewport dimensions. | Set dimensions explicitly when consistent output matters. |
| Format and quality | PNG preserves lossless detail; JPEG and WebP may reduce file size depending on content and provider behavior. | Supported formats, MIME type, and any format-specific quality option. |
| Page readiness | Dynamic pages may render content after initial navigation. | Use documented wait controls and allow enough time within your HTTP timeout. |
| Response mode | Some integrations consume bytes immediately; others work better with a stored result reference. | Document whether the response is image data, JSON, or a URL. |
Urlbox’s Ruby example illustrates provider-specific options such as full-page capture, dimensions, and quality. ScreenshotOne documents its own option set and formats. Consult the active documentation when implementing or changing options; don’t transfer parameter names between providers without checking.
7. Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| 401 or 403 response | Missing, invalid, or incorrectly transmitted key. | Confirm the secret is present in the Rails server environment and use the provider’s documented authentication method. Do not print the key in logs. |
| Successful HTTP status but file is not an image | The response may be JSON, HTML, or a configured URL/reference response. | Check Content-Type and the provider’s response mode before saving. Parse documented JSON errors rather than writing them to an image file. |
| 400 response | Malformed URL or invalid/unsupported option name or value. | Inspect the provider’s error code/message and options reference; encode query parameters rather than concatenating raw URL text. |
| Timeout | The target or rendering process did not finish before the client timeout. | Set an appropriate read timeout, configure documented readiness controls, and move non-critical captures to Active Job. Do not assume a universal capture duration. |
| Blank or incomplete capture | The page may need additional rendering time, client-side content, or a different viewport. | Check the target in a browser, set viewport dimensions explicitly, and use provider-supported wait options where needed. |
| Image content is corrupted | Bytes were treated as text or an error payload was saved as an image. | Keep the response body binary, validate status and MIME type, and use binary file writes. |
| Key appears in logs or browser tools | The request was made from public client code or query strings were logged. | Make the provider call from Rails, keep credentials in server-side secrets, and review request logging for sensitive parameters. |
8. Performance, reliability, and cost
- Latency: A capture requires a remote request and page rendering. Avoid holding a normal page response open for work that can run asynchronously. No universal duration is supported by the reviewed sources.
- Reliability: Set connection and read timeouts, check HTTP status and response type, track job state, and use bounded retries only for errors that may be transient. Log provider status and request identifiers when available, but redact credentials and sensitive URLs.
- Storage: Image bytes consume storage and may be large, especially for full-page captures. Choose output dimensions and format for the actual feature, and define a retention policy appropriate to your application.
- Cost: Provider pricing, quotas, and plan limits vary and were not verified in the cited research. Check the provider’s current plan and usage terms before launch. Consider caching or reusing captures when the underlying page does not need a fresh render each time.
Or skip the browser setup
ScreenshotNeo is a website screenshot API and MCP server. A single GET request can return PNG, JPEG, WebP, or PDF. Its API options also include full-page capture, CSS selector capture, custom CSS and JavaScript, waiting controls, and more; see the ScreenshotNeo API documentation.
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
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)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
- Cookie banners are accepted and removed, along with known newsletter popups and chat widgets, before the screenshot; each step can be turned off.
- Bot checks, blank pages, failed loads, timeouts, and cache hits are never billed. Responses identify the page verdict and billing status in headers.
- An MCP server lets AI agents using Claude, Cursor, or another MCP client take screenshots, get page information, and capture PDFs.
- The free plan includes 1,000 screenshots per month with no card; paid plans start at $5 for 3,000 screenshots.
Sign up for ScreenshotNeo’s free plan to get 1,000 screenshots a month with no card.
FAQ
Can Rails take screenshots without an external service?
Rails’ built-in screenshot helper is for system-test debugging of the app in a test browser. For arbitrary remote websites from a deployed Rails feature, use a rendering service or operate your own browser-rendering setup.
Can I return the screenshot directly from a controller?
Yes, if the response time and size suit the request. Set the response content type from the validated image type and return the bytes. For slower captures, queue a job and let the client retrieve the result later.
Should the provider key be in the browser?
No. Keep it in server-side configuration and have Rails call the provider over HTTPS.
Can I use ScreenshotOne or Urlbox parameters with another API?
Only where the other provider documents compatible names and behavior. Each provider defines its own options and response contract.


