How to Take a Website Screenshot in Ruby (Without Selenium)
You're building a Rails app that needs screenshots. You reach for Selenium and suddenly you're managing headless browsers, handling timeouts, and scaling test suites.
There's a simpler way: use a screenshot API.
One HTTP request. Binary PNG response. No browser overhead.
The Basic Pattern: Net::HTTP Binary Download
require 'net/http'
require 'json'
def take_screenshot(url, output_path)
uri = URI('https://api.pagebolt.dev/v1/screenshot')
request = Net::HTTP::Post.new(uri)
request['Authorization'] = "Bearer #{ENV['PAGEBOLT_API_KEY']}"
request['Content-Type'] = 'application/json'
request.body = JSON.generate({
url: url,
format: 'png',
width: 1280,
height: 720,
fullPage: true,
blockBanners: true
})
http = Net::HTTP.new(uri.hostname, uri.port)
http.use_ssl = true
response = http.request(request)
if response.code.to_i == 200
# Response is binary PNG — write directly to file
File.write(output_path, response.body)
puts "Screenshot saved to #{output_path}"
else
raise "API error: #{response.code}"
end
end
# Usage
take_screenshot('https://example.com', 'screenshot.png')
Key points:
-
response.bodyis binary PNG data (not JSON) -
File.write()handles binary content correctly - No JSON decoding — write bytes directly to disk
Production Pattern: Reusable Client Class
require 'net/http'
require 'json'
class PageBoltClient
def initialize(api_key)
@api_key = api_key
@base_uri = URI('https://api.pagebolt.dev/v1')
end
def screenshot(options = {})
url = options[:url]
output_path = options[:path]
raise 'url is required' unless url
uri = URI.join(@base_uri, 'screenshot')
request = Net::HTTP::Post.new(uri)
request['Authorization'] = "Bearer #{@api_key}"
request['Content-Type'] = 'application/json'
request.body = JSON.generate({
url: url,
format: options[:format] || 'png',
width: options[:width] || 1280,
height: options[:height] || 720,
fullPage: options[:fullPage] != false,
blockBanners: options[:blockBanners] != false,
blockAds: options[:blockAds] || false,
darkMode: options[:darkMode] || false
})
http = Net::HTTP.new(uri.hostname, uri.port)
http.use_ssl = true
response = http.request(request)
unless response.code.to_i == 200
raise "API error: #{response.code} #{response.body}"
end
if output_path
File.write(output_path, response.body)
return { success: true, path: output_path }
end
{ success: true, data: response.body }
end
def batch(urls, output_dir, options = {})
Dir.mkdir(output_dir) unless Dir.exist?(output_dir)
results = urls.map.with_index do |url, idx|
filename = "#{Time.now.to_i}-#{idx}.png"
filepath = File.join(output_dir, filename)
screenshot(options.merge(url: url, path: filepath))
{ url: url, path: filepath }
end
results
end
end
# Usage
client = PageBoltClient.new(ENV['PAGEBOLT_API_KEY'])
# Single screenshot
client.screenshot(
url: 'https://example.com',
fullPage: true,
path: 'homepage.png'
)
# Batch screenshots
urls = ['https://example.com', 'https://example.com/about', 'https://example.com/pricing']
results = client.batch(urls, './screenshots')
puts "Batch complete: #{results.length} screenshots"
Rails Integration: Controller Action
Use PageBolt to generate screenshots on demand:
# app/controllers/screenshots_controller.rb
class ScreenshotsController < ApplicationController
def create
@client = PageBoltClient.new(ENV['PAGEBOLT_API_KEY'])
url = params[:url]
return render json: { error: 'url required' }, status: 400 unless url
result = @client.screenshot(
url: url,
fullPage: params[:fullPage] != 'false',
path: Rails.root.join('tmp', "screenshot-#{Time.now.to_i}.png")
)
send_file(result[:path], type: 'image/png', disposition: 'inline')
end
end
# config/routes.rb
post '/screenshots', to: 'screenshots#create'
Test it:
curl -X POST http://localhost:3000/screenshots \
-d "url=https://example.com&fullPage=true" \
> screenshot.png
Real Use Case: Website Monitoring Script
Monitor websites for changes:
require 'digest'
class SiteMonitor
def initialize(api_key)
@client = PageBoltClient.new(api_key)
end
def check(url)
baseline_file = "baseline-#{Digest::MD5.hexdigest(url)}.png"
current_file = "current-#{Digest::MD5.hexdigest(url)}.png"
# First run: create baseline
unless File.exist?(baseline_file)
@client.screenshot(url: url, path: baseline_file)
puts "Baseline created for #{url}"
return
end
# Subsequent runs: compare
@client.screenshot(url: url, path: current_file)
baseline_hash = Digest::MD5.file(baseline_file).hexdigest
current_hash = Digest::MD5.file(current_file).hexdigest
if baseline_hash != current_hash
puts "⚠️ #{url} has changed!"
puts "Baseline: #{baseline_hash}"
puts "Current: #{current_hash}"
return true
end
puts "✓ #{url} unchanged"
false
end
end
# Usage: run daily via cron
monitor = SiteMonitor.new(ENV['PAGEBOLT_API_KEY'])
['https://example.com', 'https://example.com/status'].each { |url| monitor.check(url) }
Error Handling Patterns
class PageBoltClient
class APIError < StandardError; end
class RateLimitError < StandardError; end
def screenshot_with_retry(options = {}, max_retries = 3)
retries = 0
begin
screenshot(options)
rescue RateLimitError => e
if retries < max_retries
wait_time = 1000 * (2 ** retries)
puts "Rate limited. Waiting #{wait_time}ms..."
sleep(wait_time / 1000.0)
retries += 1
retry
end
raise
rescue => e
puts "Error: #{e.message}"
raise
end
end
private
def request_screenshot(uri, request)
http = Net::HTTP.new(uri.hostname, uri.port)
http.use_ssl = true
response = http.request(request)
case response.code.to_i
when 200
response
when 429
raise RateLimitError, "Rate limit exceeded"
else
raise APIError, "API error: #{response.code} #{response.body}"
end
end
end
curl + Ruby Comparison
curl approach:
curl -X POST https://api.pagebolt.dev/v1/screenshot \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"url":"https://example.com","format":"png","fullPage":true}' \
> screenshot.png
Ruby approach:
client = PageBoltClient.new(ENV['PAGEBOLT_API_KEY'])
client.screenshot(url: 'https://example.com', path: 'screenshot.png')
Both hit the same API. Ruby gives you:
- ✅ Type-safe options
- ✅ Reusable client
- ✅ Error handling
- ✅ Batch processing
- ✅ Production patterns (retry logic, monitoring)
Testing: Mock the API
require 'minitest/autorun'
class PageBoltClientTest < Minitest::Test
def test_screenshot_saves_file
client = PageBoltClient.new('test-key')
# Stub Net::HTTP
mock_response = MiniTest::Mock.new
mock_response.expect(:code, '200')
mock_response.expect(:body, "\x89PNG\r\n\x1a\n...")
Net::HTTP.stub(:request, mock_response) do
result = client.screenshot(
url: 'https://example.com',
path: 'test.png'
)
assert result[:success]
assert File.exist?('test.png')
end
end
end
Pricing
| Plan | Requests/Month | Cost | Best For |
|---|---|---|---|
| Free | 100 | $0 | Learning, low-volume projects |
| Starter | 5,000 | $29 | Small teams, moderate use |
| Growth | 25,000 | $79 | Production apps, frequent calls |
| Scale | 100,000 | $199 | High-volume automation |
Summary
- ✅ Idiomatic Ruby with
Net::HTTP - ✅ Binary response handling with
File.write() - ✅ Reusable client class for production
- ✅ Rails integration examples
- ✅ Monitoring and error handling patterns
- ✅ No Selenium, no browser management
- ✅ Works in tests and background jobs
Get started free: pagebolt.dev — 100 requests/month, no credit card required.
Top comments (0)