Screenshot API for Ruby — Net::HTTP, Faraday & Rails Guide 2026

Capture screenshots and generate PDFs from Ruby applications without installing a browser. Complete examples for Net::HTTP, Faraday, Rails ActiveJob, and concurrent batch processing.

Get Free API Key

Why Ruby Developers Use a Screenshot API

Ruby developers building web apps, scraping pipelines, and automation scripts regularly need screenshots and PDFs. The traditional approach is to install Puppeteer via Node.js alongside a Ruby app, then shell out to it — adding a Node.js runtime, npm dependencies, and a headless Chrome binary to a project that otherwise has none of those requirements. Running headless Chrome in a Ruby on Rails app on Heroku or Render requires custom buildpacks, bumps the slug size past the 500 MB limit, and burns dyno memory. SnapAPI replaces that entire dependency chain with a single HTTP call from any Ruby HTTP library you already use.

Net::HTTP Screenshot Example

The Ruby standard library Net::HTTP requires no gem dependencies and works in any Ruby environment. Here is a complete screenshot helper using Net::HTTP:

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

def screenshot(url, full_page: true, format: 'png')
  base = URI('https://snapapi.pics/screenshot')
  base.query = URI.encode_www_form(
    access_key: ENV['SNAPAPI_KEY'],
    url: url,
    full_page: full_page ? '1' : '0',
    format: format
  )
  response = Net::HTTP.get_response(base)
  raise "SnapAPI error #{response.code}" unless response.code == '200'
  response.body
end

# Save a PNG screenshot
image_bytes = screenshot('https://example.com')
File.binwrite('screenshot.png', image_bytes)
puts "Saved #{image_bytes.bytesize} bytes"

Faraday Screenshot Example

Faraday is the most widely used HTTP client gem in Ruby and Rails ecosystems. It provides middleware-based request/response processing and integrates cleanly with Rails apps. Here is the equivalent screenshot function with Faraday:

require 'faraday'

def screenshot_faraday(url, full_page: true)
  conn = Faraday.new(url: 'https://snapapi.pics') do |f|
    f.response :raise_error
    f.adapter Faraday.default_adapter
  end

  response = conn.get('/screenshot') do |req|
    req.params['access_key'] = ENV['SNAPAPI_KEY']
    req.params['url'] = url
    req.params['full_page'] = full_page ? '1' : '0'
    req.params['format'] = 'png'
  end

  response.body
end

bytes = screenshot_faraday('https://example.com')
File.binwrite('output.png', bytes)

# PDF generation
def pdf_from_url(url)
  conn = Faraday.new(url: 'https://snapapi.pics')
  response = conn.get('/screenshot') do |req|
    req.params['access_key'] = ENV['SNAPAPI_KEY']
    req.params['url'] = url
    req.params['format'] = 'pdf'
    req.params['full_page'] = '1'
    req.params['paper_format'] = 'A4'
  end
  response.body
end

Rails ActiveJob Background Screenshot

In a Rails app, screenshots should be generated in a background job rather than inline in a controller action. Here is a complete ActiveJob implementation that captures a screenshot and attaches it to an ActiveStorage record:

class ScreenshotJob < ApplicationJob
  queue_as :default
  retry_on StandardError, wait: :exponentially_longer, attempts: 3

  def perform(page_id, url)
    page = Page.find(page_id)
    image_bytes = capture_screenshot(url)
    page.screenshot.attach(
      io: StringIO.new(image_bytes),
      filename: "screenshot-#{page_id}.png",
      content_type: 'image/png'
    )
    page.update!(screenshot_captured_at: Time.current)
  end

  private

  def capture_screenshot(url)
    uri = URI('https://snapapi.pics/screenshot')
    uri.query = URI.encode_www_form(
      access_key: ENV['SNAPAPI_KEY'],
      url: url,
      full_page: '1',
      format: 'png',
      viewport_width: '1280',
      viewport_height: '800'
    )
    response = Net::HTTP.get_response(uri)
    raise "Screenshot failed: #{response.code}" unless response.code == '200'
    response.body
  end
end

# Enqueue from controller
ScreenshotJob.perform_later(page.id, page.url)

Concurrent Batch Screenshots with Ruby Threads

Ruby's Thread class allows concurrent HTTP requests for batch screenshot jobs. The GIL (Global Interpreter Lock) in MRI Ruby still allows I/O-bound operations like HTTP requests to run concurrently, making threading effective for screenshot batch processing:

require 'net/http'
require 'uri'

def batch_screenshots(urls, concurrency: 5)
  results = {}
  mutex = Mutex.new
  semaphore = Mutex.new
  active = 0

  threads = urls.map do |url|
    Thread.new do
      loop do
        semaphore.synchronize { break if active < concurrency; sleep 0.05 }
        semaphore.synchronize { active += 1 }
        begin
          bytes = screenshot(url)
          mutex.synchronize { results[url] = { status: :ok, bytes: bytes.bytesize } }
        rescue => e
          mutex.synchronize { results[url] = { status: :error, message: e.message } }
        ensure
          semaphore.synchronize { active -= 1 }
        end
        break
      end
    end
  end

  threads.each(&:join)
  results
end

urls = [
  'https://example.com',
  'https://ruby-lang.org',
  'https://rubyonrails.org'
]
results = batch_screenshots(urls, concurrency: 3)
results.each { |url, r| puts "#{url}: #{r[:status]} (#{r[:bytes]} bytes)" }

Scraping and Data Extraction from Ruby

SnapAPI exposes a scrape endpoint that returns cleaned HTML or markdown text and an extract endpoint that returns structured JSON from a web page. Both endpoints handle JavaScript rendering, bot detection bypass, and proxy rotation automatically. From Ruby, call the scrape endpoint to get page content without managing Nokogiri + HTTP client stacks for JavaScript-rendered sites:

def scrape_page(url)
  uri = URI('https://snapapi.pics/scrape')
  uri.query = URI.encode_www_form(
    access_key: ENV['SNAPAPI_KEY'],
    url: url,
    output: 'markdown'
  )
  response = Net::HTTP.get_response(uri)
  return nil unless response.code == '200'
  JSON.parse(response.body)
end

result = scrape_page('https://example.com/pricing')
puts result['content']  # clean markdown text of the page

Sinatra Integration

For lightweight Sinatra applications, adding a screenshot endpoint takes fewer than twenty lines. Here is a Sinatra route that accepts a URL parameter and returns a screenshot PNG inline:

require 'sinatra'
require 'net/http'
require 'uri'

get '/screenshot' do
  target_url = params[:url]
  halt 400, 'url required' if target_url.nil? || target_url.empty?

  uri = URI('https://snapapi.pics/screenshot')
  uri.query = URI.encode_www_form(
    access_key: ENV['SNAPAPI_KEY'],
    url: target_url,
    format: 'png',
    viewport_width: '1280',
    viewport_height: '800'
  )
  response = Net::HTTP.get_response(uri)
  halt 502, 'Screenshot failed' unless response.code == '200'

  content_type 'image/png'
  response.body
end

Environment Configuration and Key Storage

Store your SnapAPI key in an environment variable and never commit it to source control. In Rails, use the encrypted credentials system: run rails credentials:edit and add snapapi_key: your_key_here, then reference it as Rails.application.credentials.snapapi_key. For non-Rails Ruby apps, use the dotenv gem — add gem 'dotenv' to your Gemfile, create a .env file with SNAPAPI_KEY=your_key_here, add .env to .gitignore, and call Dotenv.load at app startup. For production deployments on Heroku, Render, or Fly.io, set the environment variable through the platform's secrets management interface rather than a dotenv file. Rotate the key via the SnapAPI dashboard if it is ever accidentally exposed.

Using the Official SnapAPI Ruby SDK

The official SnapAPI Ruby gem wraps the HTTP API with a typed interface, handles parameter serialization, and provides built-in error classes for clean rescue blocks. Install it with gem 'snapapi' in your Gemfile or gem install snapapi for standalone scripts. The gem mirrors the API surface: SnapAPI::Client.new(api_key: ENV['SNAPAPI_KEY']) creates a client instance, and methods like client.screenshot(url: 'https://example.com', full_page: true, format: 'png') return a response object with .body for binary content and .to_file('output.png') for direct file saving. The extract method accepts a schema hash directly without JSON serialization boilerplate. Because the SDK uses Net::HTTP under the hood, it adds no new runtime dependencies to your project and works on any Ruby version 2.7 and above without requiring native extensions or system-level browser installations.

Generating PDFs with Custom Page Headers and Footers

SnapAPI's PDF endpoint supports custom headers and footers injected as HTML strings. This is essential for invoice generation, report delivery, and compliance document archiving where every page needs a company logo, document title, and page number. Pass the header and footer as URL-encoded HTML to the pdf_header and pdf_footer parameters. The header and footer run in a separate rendering context from the main page, so they can reference CSS variables for page number ({{page}}) and total pages ({{pages}}). For invoice generation in a Rails app, render your invoice Liquid or ERB template to an HTML string, pass it to the SnapAPI screenshot endpoint with format=pdf, and stream the resulting PDF bytes directly to the HTTP response with send_data and content_type: 'application/pdf'. The entire round-trip from request to PDF delivery typically takes 1.5 to 3 seconds, which is fast enough for synchronous delivery in a controller action for most invoice sizes.

Deploying Ruby Apps That Use SnapAPI on Heroku and Render

One of the primary reasons Ruby developers choose SnapAPI over self-hosted headless Chrome is Heroku and Render compatibility. Heroku's standard dynos cap at 512 MB RAM, and Render's starter instances at 512 MB as well. A single Chromium instance consumes 400 to 600 MB of RAM, which means a self-hosted headless browser approach is completely incompatible with entry-level cloud deployments. With SnapAPI, your Ruby app makes outbound HTTP requests — no browser process, no extra RAM, no custom buildpacks. The Heroku deploy is identical to any other Ruby app: add the Gemfile entry, set the SNAPAPI_KEY config var via heroku config:set SNAPAPI_KEY=your_key, and deploy normally. This also means screenshot functionality works on Heroku's free and eco tiers without any slug size concerns, and on any managed Ruby hosting platform that allows outbound HTTP connections, which is essentially all of them.

Screenshot Rate Limiting and Webhook Delivery

High-volume Ruby applications that generate screenshots at user request time should protect against runaway usage with a per-user rate limit before the SnapAPI call. Use Redis with a sliding window counter keyed by user ID to enforce a per-minute or per-hour cap on screenshot generation at the application level, independent of the API-level rate limit. When screenshot volume is unpredictable — such as when a user triggers a bulk export of many pages — move generation to a background queue using Sidekiq or GoodJob. Each job captures one screenshot, stores the result in S3, and marks the item as complete. A polling endpoint or Action Cable channel notifies the frontend when all screenshots are ready. This prevents a single large export from blocking other users or exhausting your API quota in a burst, and it gives the user a progress indicator rather than a browser timeout on a long-running synchronous request.