Capture any website as PNG, JPEG, WebP, or PDF with one Net::HTTP request. No Selenium, no Ferrum, no Chrome in your Docker image. Works in Rails, Sidekiq, and plain scripts.
200 free screenshots/month. No credit card required.
The whole API is a GET request. Your key goes in the X-API-Key header, options go in query parameters, the image comes back as the response body.
require "net/http"
require "json"
uri = URI("https://app.snap-render.com/v1/screenshot")
uri.query = URI.encode_www_form(
url: "https://example.com",
format: "png",
width: 1280,
height: 800
)
request = Net::HTTP::Get.new(uri)
request["X-API-Key"] = ENV.fetch("SNAPRENDER_API_KEY")
response = Net::HTTP.start(uri.host, uri.port, use_ssl: true, read_timeout: 60) do |http|
http.request(request)
end
unless response.is_a?(Net::HTTPSuccess)
error = JSON.parse(response.body)["error"]
raise "#{error["code"]}: #{error["message"]}"
end
File.binwrite("screenshot.png", response.body)
class SnapRender
ENDPOINT = URI("https://app.snap-render.com/v1/screenshot")
def initialize(api_key = ENV.fetch("SNAPRENDER_API_KEY"))
@api_key = api_key
end
# Returns [bytes, headers]. Raises on non-2xx after one retry for RATE_LIMITED.
def capture(url, **options)
uri = ENDPOINT.dup
uri.query = URI.encode_www_form({ url: url }.merge(options))
request = Net::HTTP::Get.new(uri)
request["X-API-Key"] = @api_key
attempts = 0
begin
attempts += 1
response = Net::HTTP.start(uri.host, uri.port, use_ssl: true, read_timeout: 60) { |http| http.request(request) }
return [response.body, response.to_hash] if response.is_a?(Net::HTTPSuccess)
error = JSON.parse(response.body)["error"]
if error["code"] == "RATE_LIMITED" && attempts < 2
sleep 60 # per-minute burst limit; QUOTA_EXCEEDED is monthly and should not be retried
retry
end
raise "SnapRender #{response.code} #{error["code"]}: #{error["message"]}"
end
end
end
bytes, headers = SnapRender.new.capture(
"https://example.com",
format: "jpeg", quality: 85, full_page: true,
device: "iphone_15_pro", dark_mode: true,
cache: true, cache_ttl: 3600
)
File.binwrite("mobile-dark.jpg", bytes)
puts "cache: #{headers["x-cache"]&.first}, credits left: #{headers["x-remaining-credits"]&.first}"
pdf, _ = SnapRender.new.capture(
"https://example.com/report",
format: "pdf", full_page: true, width: 1024
)
File.binwrite("report.pdf", pdf)
On the wire this is the same GET request with format=pdf; the PDF is a page image, not a text layer.
Ads and cookie banners are blocked by default. The PDF is a rendered page image, not a print-stylesheet layout. Full parameter reference in the docs.
Ferrum and selenium-webdriver are good tools for system tests, where a browser is the point. As a way to produce an image of a URL in production they carry a lot: a Chrome build in every Docker image, chromedriver versions that must match, a font stack, and a browser process inside your Sidekiq worker that grows until the container is restarted. A timeout on one slow page can stall a whole queue.
With a screenshot API the browser runs on our side. Ruby sends one request from the standard library and receives the finished image. Cookie banners and ads are removed, full-page capture handles lazy loading, and your Gemfile and Dockerfile do not change.
On Rails, the Rails guide shows the Active Job plus Active Storage version: capture in a background job, attach the bytes to a record, serve from your storage service.
Because the integration is a GET request, it runs anywhere Ruby can open an outbound HTTPS connection. No buildpack for Chrome, no apt-get list in the Dockerfile, and no memory headroom reserved for a browser next to Puma.
| SnapRender API | Ferrum / Selenium in production | |
|---|---|---|
| Dependencies | Net::HTTP (stdlib) | Gem plus Chrome plus chromedriver |
| Docker image | Unchanged | Hundreds of MB larger |
| Worker memory | Not your problem | Browser process per job |
| Cookie banners and ads | Blocked by default | Custom code per site |
| Caching | cache=true, hits are free | Build it yourself |
| Browser updates | Handled for you | Yours to test and ship |
Want to see the output first? Try the free website screenshot tool. It uses the same rendering engine as the API.
Build a URI for the /v1/screenshot endpoint with the target in the url parameter, send a Net::HTTP GET with your key in the X-API-Key header, and write response.body to a file. The page renders in real Chrome on SnapRender's servers, so your Ruby process never starts a browser.
No. The only dependency is Net::HTTP from the standard library. There is no chromedriver to match to a Chrome version, no browser to add to your Docker image, and no Xvfb on the server.
Not yet. The API is a plain HTTP endpoint, so the examples here use Net::HTTP and work unchanged with Faraday or HTTParty. A gem is planned; the request format will not change when it ships.
Wrap response.body in StringIO and call record.image.attach(io:, filename:, content_type:). Do the capture inside an Active Job so the request cycle is not blocked by a render that takes a few seconds. The Rails guide on this site walks through the job.
Yes. The free plan includes 200 screenshots per month with every feature: all formats including PDF, full-page capture, device emulation, dark mode, and caching. No credit card is required to sign up.
The same API has quickstarts for other languages.
Related guides: Screenshots in Rails with Active Storage and serverless runtimes.
Or skip the setup entirely
Everything in this guide is one GET request with SnapRender. No browser to babysit, no timeouts to tune.
200 screenshots a month free. First render in under a minute.
Grab a key, paste the snippet, done. 200 free screenshots per month, no credit card.