How to Generate PDFs from URLs with Ruby

Published April 4, 2026 · 6 min read · URLSnap Team

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).

Why Not wkhtmltopdf or Prawn?

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.

Get a Free API Key

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}"

Convert a URL to PDF

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'
📄 Supported formats: A4, Letter, Legal, A3, Tabloid. Default is A4.

Convert HTML to 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'

Rails Controller — Send PDF to Browser

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

Sidekiq Background Job

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')
💡 Free plan: 20 req/day. Starter ($9/mo): 500 req/day. Pro ($29/mo): 5,000 req/day. Upgrade at urlsnap.dev/#pricing.

Check Your Quota

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

Error Handling

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

Start generating PDFs with Ruby today

Free tier: 20 PDFs/day. No credit card, no wkhtmltopdf binary, no Prawn boilerplate.

Get your free API key →