Need to generate PDFs from web pages or ERB templates in Ruby? This tutorial covers how to convert any URL to a PDF using the URLSnap API — no wkhtmltopdf binary, no Prawn layout headaches, no Selenium. Works in plain Ruby, Rails, Sinatra, and background jobs (Sidekiq, GoodJob).
wkhtmltopdf uses an old Qt-based WebKit engine that doesn't support modern CSS. Prawn is a low-level PDF composer — not ideal for HTML-to-PDF. URLSnap renders with headless Chromium, so your full CSS and JavaScript renders faithfully.
require 'net/http'
require 'json'
require 'uri'
def register_api_key(email)
uri = URI('https://urlsnap.dev/api/register')
resp = Net::HTTP.post(uri, { email: email }.to_json, 'Content-Type' => 'application/json')
JSON.parse(resp.body)['key']
end
key = register_api_key('you@example.com')
puts "API key: #{key}"
require 'net/http'
require 'uri'
API_KEY = 'us_your_key_here'
def url_to_pdf(target_url, output_path, format: 'A4')
params = URI.encode_www_form(url: target_url, format: format)
uri = URI("https://urlsnap.dev/api/pdf?#{params}")
req = Net::HTTP::Get.new(uri)
req['x-api-key'] = API_KEY
Net::HTTP.start(uri.host, uri.port, use_ssl: true) do |http|
resp = http.request(req)
raise "API error #{resp.code}: #{resp.body}" unless resp.is_a?(Net::HTTPSuccess)
File.binwrite(output_path, resp.body)
end
end
# Basic A4 PDF
url_to_pdf('https://example.com', 'example.pdf')
puts 'Saved example.pdf'
# Letter-size PDF
url_to_pdf('https://ruby-lang.org', 'ruby-lang.pdf', format: 'Letter')
puts 'Saved ruby-lang.pdf'
POST an HTML string — perfect for ERB invoice templates rendered server-side:
require 'net/http'
require 'json'
require 'uri'
API_KEY = 'us_your_key_here'
def html_to_pdf(html_content, output_path, format: 'A4')
uri = URI('https://urlsnap.dev/api/pdf')
body = { html: html_content, format: format }.to_json
req = Net::HTTP::Post.new(uri)
req['x-api-key'] = API_KEY
req['Content-Type'] = 'application/json'
req.body = body
Net::HTTP.start(uri.host, uri.port, use_ssl: true) do |http|
resp = http.request(req)
raise "API error #{resp.code}: #{resp.body}" unless resp.is_a?(Net::HTTPSuccess)
File.binwrite(output_path, resp.body)
end
end
invoice_html = <<~HTML
<!DOCTYPE html>
<html>
<head>
<style>
body { font-family: sans-serif; padding: 40px; color: #111; }
h1 { color: #dc2626; }
table { width: 100%; border-collapse: collapse; margin-top: 20px; }
th, td { padding: 10px; border-bottom: 1px solid #eee; text-align: left; }
.total { font-weight: bold; }
</style>
</head>
<body>
<h1>Invoice #INV-2026-001</h1>
<p>Bill to: Acme Corp</p>
<table>
<tr><th>Item</th><th>Amount</th></tr>
<tr><td>API Starter Plan</td><td>$9.00</td></tr>
<tr class="total"><td>Total</td><td>$9.00</td></tr>
</table>
</body>
</html>
HTML
html_to_pdf(invoice_html, 'invoice.pdf')
puts 'Saved invoice.pdf'
Stream the PDF directly to the user's browser from a Rails action:
# app/controllers/reports_controller.rb
class ReportsController < ApplicationController
API_KEY = ENV.fetch('URLSNAP_API_KEY')
def download
target_url = params.require(:url)
params_str = URI.encode_www_form(url: target_url, format: 'A4')
uri = URI("https://urlsnap.dev/api/pdf?#{params_str}")
req = Net::HTTP::Get.new(uri)
req['x-api-key'] = API_KEY
Net::HTTP.start(uri.host, uri.port, use_ssl: true) do |http|
resp = http.request(req)
unless resp.is_a?(Net::HTTPSuccess)
render json: { error: "PDF generation failed: #{resp.code}" },
status: :bad_gateway
return
end
send_data resp.body,
filename: 'report.pdf',
type: 'application/pdf',
disposition: 'attachment'
end
end
end
Generate PDFs asynchronously and save to storage (e.g. Active Storage or S3):
# app/jobs/generate_pdf_job.rb
class GeneratePdfJob
include Sidekiq::Job
API_KEY = ENV.fetch('URLSNAP_API_KEY')
def perform(report_id, target_url)
params_str = URI.encode_www_form(url: target_url, format: 'A4')
uri = URI("https://urlsnap.dev/api/pdf?#{params_str}")
req = Net::HTTP::Get.new(uri)
req['x-api-key'] = API_KEY
Net::HTTP.start(uri.host, uri.port, use_ssl: true) do |http|
resp = http.request(req)
raise "PDF error #{resp.code}" unless resp.is_a?(Net::HTTPSuccess)
report = Report.find(report_id)
report.pdf_file.attach(
io: StringIO.new(resp.body),
filename: "report-#{report_id}.pdf",
content_type: 'application/pdf'
)
report.update!(pdf_generated_at: Time.current)
end
end
end
# Enqueue it:
# GeneratePdfJob.perform_async(report.id, 'https://example.com/report')
require 'net/http'
require 'json'
require 'uri'
API_KEY = 'us_your_key_here'
uri = URI('https://urlsnap.dev/api/me')
req = Net::HTTP::Get.new(uri)
req['x-api-key'] = API_KEY
Net::HTTP.start(uri.host, uri.port, use_ssl: true) do |http|
resp = http.request(req)
info = JSON.parse(resp.body)
puts "Plan: #{info['plan']}"
puts "Used today: #{info['requests_today']}/#{info['daily_limit']}"
puts "Total all-time: #{info['requests_total']}"
end
def safe_pdf(target_url, output_path)
params_str = URI.encode_www_form(url: target_url, format: 'A4')
uri = URI("https://urlsnap.dev/api/pdf?#{params_str}")
req = Net::HTTP::Get.new(uri)
req['x-api-key'] = API_KEY
Net::HTTP.start(uri.host, uri.port, use_ssl: true) do |http|
resp = http.request(req)
case resp.code.to_i
when 200
File.binwrite(output_path, resp.body)
true
when 429
warn 'Daily limit reached. Upgrade at urlsnap.dev/#pricing'
false
when 401
warn 'Invalid API key'
false
else
warn "PDF error #{resp.code}: #{resp.body}"
false
end
end
rescue Net::OpenTimeout, Net::ReadTimeout => e
warn "Timeout: #{e.message}"
false
end
Free tier: 20 PDFs/day. No credit card, no wkhtmltopdf binary, no Prawn boilerplate.
Get your free API key →