Screenshot API · Ruby

Screenshot API for Ruby.

Shotium is a hosted screenshot API you call from Ruby with stdlib net/http: no Selenium, no Ferrum, no Chrome binary in your Gemfile or your Docker image. Send a URL, get back PNG, JPEG or WebP bytes — with full-page capture, template-based OG images, and HMAC-signed URLs you can embed in public HTML.

How do I take a screenshot in Ruby?

Stdlib only — no gems. Sign in, create an API key, and this runs as-is:

require "net/http"

uri = URI("https://api.shotium.com/v1/screenshot")
uri.query = URI.encode_www_form(url: "https://example.com", format: "png")

req = Net::HTTP::Get.new(uri)
req["Authorization"] = "Bearer #{ENV.fetch('SHOTIUM_KEY')}"

res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true, read_timeout: 60) do |http|
  http.request(req)
end
raise "render failed: #{res.code}" unless res.is_a?(Net::HTTPSuccess)

File.binwrite("shot.png", res.body)

Full-page screenshots and viewport control

Set full_page to capture the entire scroll height (up to 20,000px). URI.encode_www_form serializes Ruby's true to the literal the API expects:

uri.query = URI.encode_www_form(
  url: "https://news.ycombinator.com",
  full_page: true,
  format: "webp",
  quality: 90,
  width: 1280,
)

Generate OG images from templates

POST typed parameters into one of five built-in templates and get a finished 1200×630 social card back — no HTML to write:

require "json"
require "net/http"

uri = URI("https://api.shotium.com/v1/og-image")
req = Net::HTTP::Post.new(uri)
req["Authorization"] = "Bearer #{ENV.fetch('SHOTIUM_KEY')}"
req["Content-Type"] = "application/json"
req.body = JSON.generate(
  template: "blog",
  params: { title: "Shipping fast without breaking things", author: "Ada L." },
  format: "png",
)

res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true, read_timeout: 60) do |http|
  http.request(req)
end
File.binwrite("og.png", res.body)

How do I embed OG images without exposing my API key?

Sign the query string with your signing secret instead of sending your key. ERB::Util.url_encode is RFC 3986 (CGI.escape is not — it turns spaces into +), and Hash#sort gives byte-order keys:

require "openssl"
require "erb"

params = { "template" => "minimal", "title" => "Less, but better.", "uid" => "YOUR-UID" }

canonical = params.sort.map { |k, v|
  "#{ERB::Util.url_encode(k)}=#{ERB::Util.url_encode(v)}"
}.join("&")

sig = OpenSSL::HMAC.hexdigest("SHA256", ENV.fetch("SHOTIUM_SIGNING_SECRET"), canonical)
url = "https://api.shotium.com/v1/og-image?#{canonical}&sig=#{sig}"

Error handling

Every error is RFC 9457 problem+json with a stable type field you can branch on. Failed renders are never billed:

res = http.request(req)
unless res.is_a?(Net::HTTPSuccess)
  problem = JSON.parse(res.body) # RFC 9457 problem+json
  case problem["type"]
  when "rate_limited"
    wait = res["Retry-After"].to_i # back off, then retry
  when "render_timeout"
    # site took >30s — retry or skip
  end
end

Parameters at a glance

ParamDefaultNotes
urlrequiredhttp(s) URL to render
width / height1280 × 800Viewport, up to 3840 × 2160
full_pagefalseFull scroll height, ≤20,000px
formatpngpng | jpeg | webp
quality801–100, lossy formats only

Full reference — auth, OG templates, rate limits, error table — in the docs. Template parameters live on /og-templates.

Frequently asked questions

Do I need Ferrum or Selenium to take screenshots in Ruby?

No. Ferrum, Watir and Selenium all need a Chrome binary installed and supervised alongside your app. Shotium runs the browsers server-side — from Ruby it's one stdlib net/http request, nothing in your Gemfile.

How do I take a full-page screenshot in Ruby?

Pass full_page: true to URI.encode_www_form — Ruby serializes it as the literal "true" the API expects. The renderer scrolls, waits for lazy-loaded content, and captures up to 20,000px. Raise read_timeout above the default for tall pages and add format: "webp".

Can I call the API from Rails background jobs?

Yes — it's plain HTTPS, so Sidekiq and ActiveJob workers work unchanged, and the binary response drops straight into Active Storage. Rate limit is 60 requests/minute per key, so cap job concurrency accordingly.

How should Ruby code react to a failed render?

Test with res.is_a?(Net::HTTPSuccess) before binwrite — net/http returns error responses as ordinary objects, so an unchecked body lands in your .png. Failures carry RFC 9457 problem+json: rate_limited means sleep for the Retry-After header, render_timeout means the target took over 30 seconds. Failed renders are never billed.

Try it on your own pages

Sign-up is GitHub OAuth and comes with 100 free render credits — no card. Failed renders never bill. Plans from $15/month on pricing.

Also available for: Python · Node.js · PHP · Go · Java · cURL