Screenshot API for Ruby on Rails: Screenshots and PDFs Without wkhtmltopdf
Generating PDFs and screenshots in Rails is one of those problems that looks simple until you actually try to deploy it. wkhtmltopdf requires a system binary. WickedPDF wraps wkhtmltopdf and adds a Rails DSL on top, but you're still shipping a binary in Docker and fighting with font paths in production. Prawn gives you programmatic PDFs, but it's a layout engine — you write Ruby to position every line of text, which means your HTML invoice template and your PDF invoice are two separate things to maintain.
The REST API approach sidesteps all of this. You send a URL or an HTML string, get back a PDF or PNG. No binary dependencies. No DSL. No Dockerfile changes.
Why Not wkhtmltopdf or WickedPDF?
- System dependency — wkhtmltopdf must be installed on every machine (dev, CI, production). Docker images grow by 100–200 MB. ARM vs x86 issues are common.
- Maintenance — wkhtmltopdf is no longer actively maintained. Its Chromium rendering engine is old and renders modern CSS inconsistently.
-
WickedPDF adds a DSL — you configure headers, footers, margins, and page breaks through Ruby options instead of CSS. CSS
@media printrules still exist but feel bolted-on. - Prawn is a layout engine, not a renderer — it doesn't understand HTML. You can't take an existing ERB invoice template and hand it to Prawn.
A screenshot API uses a real, maintained headless Chromium instance. Your ERB template renders the same in the browser and in the PDF.
Setup
Add Faraday to your Gemfile if you don't already have it:
# Gemfile
gem 'faraday'
Store your API key in credentials:
rails credentials:edit
pagebolt:
api_key: your_api_key_here
A PageBolt Service Object
Create app/services/pagebolt_client.rb:
# app/services/pagebolt_client.rb
class PageboltClient
BASE_URL = "https://pagebolt.dev/api/v1".freeze
def initialize
@api_key = Rails.application.credentials.dig(:pagebolt, :api_key)
@conn = Faraday.new(url: BASE_URL) do |f|
f.request :json
f.response :raise_error
f.adapter Faraday.default_adapter
end
end
def screenshot(url:, full_page: false, **opts)
post("/screenshot", { url: url, fullPage: full_page }.merge(opts))
end
def pdf(url: nil, html: nil, **opts)
raise ArgumentError, "Provide url or html" if url.nil? && html.nil?
payload = html ? { html: html } : { url: url }
post("/pdf", payload.merge(opts))
end
private
def post(path, payload)
response = @conn.post(path, payload) do |req|
req.headers["x-api-key"] = @api_key
end
response.body
end
end
This gives you a thin, reusable wrapper. Call it from any service, job, or controller.
Use Case 1: Invoice PDF Generation
You have an ERB template at app/views/invoices/show.html.erb. You want a pixel-perfect PDF that looks exactly like the browser version.
# app/services/invoice_pdf_service.rb
class InvoicePdfService
def initialize(invoice)
@invoice = invoice
end
def generate
html = ApplicationController.renderer.render(
template: "invoices/show",
assigns: { invoice: @invoice },
layout: "pdf"
)
PageboltClient.new.pdf(html: html, pdfOptions: { format: "A4" })
end
end
Use a dedicated layouts/pdf.html.erb layout that inlines CSS (external stylesheets aren't loaded when you pass raw HTML). Then call it from a controller action:
# app/controllers/invoices_controller.rb
def download
invoice = Invoice.find(params[:id])
pdf_bytes = InvoicePdfService.new(invoice).generate
send_data pdf_bytes,
filename: "invoice-#{invoice.number}.pdf",
type: "application/pdf",
disposition: "attachment"
end
No wkhtmltopdf installed. No Prawn layout code. Your ERB template is the single source of truth.
Use Case 2: ActionMailer Attachment
Send the PDF as an email attachment when an invoice is finalized:
# app/mailers/invoice_mailer.rb
class InvoiceMailer < ApplicationMailer
def finalized(invoice)
@invoice = invoice
pdf_bytes = InvoicePdfService.new(invoice).generate
attachments["invoice-#{invoice.number}.pdf"] = {
mime_type: "application/pdf",
content: pdf_bytes
}
mail(
to: invoice.client_email,
subject: "Invoice ##{invoice.number} from #{invoice.account_name}"
)
end
end
Call it from your job:
InvoiceMailer.finalized(invoice).deliver_later
Use Case 3: Website Monitoring with Active Job
Capture a daily screenshot of your app to detect visual regressions — broken layouts, 500 pages, missing assets:
# app/jobs/visual_monitor_job.rb
class VisualMonitorJob < ApplicationJob
queue_as :default
def perform(url)
client = PageboltClient.new
png_bytes = client.screenshot(url: url, full_page: true, blockBanners: true)
date_stamp = Date.today.iso8601
path = Rails.root.join("tmp", "snapshots", "#{date_stamp}.png")
FileUtils.mkdir_p(path.dirname)
File.binwrite(path, png_bytes)
Rails.logger.info "Snapshot saved: #{path}"
end
end
Schedule it with Sidekiq Cron or Whenever:
# config/schedule.rb (whenever)
every 1.day, at: "9:00 am" do
runner "VisualMonitorJob.perform_later('https://yourapp.com')"
end
Using Net::HTTP Instead of Faraday
If you'd rather avoid the Faraday dependency, the standard library works fine:
require "net/http"
require "uri"
require "json"
def fetch_pdf(html_string)
uri = URI("https://pagebolt.dev/api/v1/pdf")
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = true
request = Net::HTTP::Post.new(uri.path)
request["x-api-key"] = Rails.application.credentials.dig(:pagebolt, :api_key)
request["Content-Type"] = "application/json"
request.body = JSON.generate({ html: html_string, pdfOptions: { format: "A4" } })
response = http.request(request)
raise "PageBolt error: #{response.code}" unless response.is_a?(Net::HTTPSuccess)
response.body
end
Both approaches return the raw binary. Pass it to send_data or write it to disk.
A Concern for DRY Integration
If you need PageBolt calls across multiple models or controllers, wrap it in a concern:
# app/controllers/concerns/pdf_exportable.rb
module PdfExportable
extend ActiveSupport::Concern
included do
def export_pdf(html:, filename:)
pdf_bytes = PageboltClient.new.pdf(html: html)
send_data pdf_bytes,
filename: filename,
type: "application/pdf",
disposition: "attachment"
end
end
end
Free Tier
PageBolt includes 100 requests per month on the free tier — no credit card required. That's enough to build and test invoice generation, monitoring, and email attachments end to end before you commit to a paid plan.
Get started: pagebolt.dev
Top comments (0)