Guide
Generate a PDF from HTML in Ruby on Rails
Most Rails apps need a PDF eventually: an invoice, a receipt, a packing slip, a report. The classic answer is wicked_pdf plus a bundled wkhtmltopdf binary. This guide shows a lighter approach: POST your HTML to the PDFPipe API from a controller action and stream the bytes back with send_data. No system binary, no asset pipeline tricks.
The idea
Render an ERB template to an HTML string as you normally would, send that string to POST /v1/pdf, and pass the returned application/pdf bytes straight to the browser. The API does the heavy rendering, so your dynos stay small and stateless.
Render the HTML
Use render_to_string so you can reuse an existing view and layout. This keeps your invoice markup in one place instead of inlining HTML in Ruby.
# app/controllers/invoices_controller.rb
class InvoicesController < ApplicationController
def show
@invoice = Invoice.find(params[:id])
html = render_to_string(
template: "invoices/show",
layout: "pdf",
formats: [:html]
)
pdf = PdfPipe.render(html, format: "A4")
send_data pdf,
filename: "invoice-#{@invoice.number}.pdf",
type: "application/pdf",
disposition: "inline"
end
endThe API call with Net::HTTP
No gems required. Wrap the call in a small service object (a plain Ruby class under app/services) so controllers stay thin. Store the key in Rails.application.credentials or an environment variable, never in source.
# app/services/pdf_pipe.rb
require "net/http"
require "json"
require "uri"
module PdfPipe
ENDPOINT = URI("https://api.pdfpipe.xyz/v1/pdf")
def self.render(html, format: "A4")
request = Net::HTTP::Post.new(ENDPOINT)
request["Authorization"] = "Bearer #{ENV.fetch('PDFPIPE_API_KEY')}"
request["Content-Type"] = "application/json"
request.body = JSON.generate(
html: html,
options: { format: format }
)
response = Net::HTTP.start(
ENDPOINT.hostname, ENDPOINT.port, use_ssl: true
) { |http| http.request(request) }
unless response.is_a?(Net::HTTPSuccess)
raise "PDFPipe error #{response.code}: #{response.body}"
end
response.body # raw application/pdf bytes
end
endOr with Faraday
If your app already uses Faraday, the same call is a few lines. Faraday gives you connection reuse, retries, and timeouts for free.
# app/services/pdf_pipe.rb
require "faraday"
require "json"
module PdfPipe
def self.render(html, format: "A4")
conn = Faraday.new(url: "https://api.pdfpipe.xyz") do |f|
f.options.timeout = 30
end
response = conn.post("/v1/pdf") do |req|
req.headers["Authorization"] = "Bearer #{ENV.fetch('PDFPIPE_API_KEY')}"
req.headers["Content-Type"] = "application/json"
req.body = JSON.generate(
html: html,
options: { format: format }
)
end
raise "PDFPipe error #{response.status}" unless response.success?
response.body
end
endHow this compares to wicked_pdf
wicked_pdf is a solid, well loved gem. It wraps wkhtmltopdf, which renders locally with no network call. That is a real advantage if you cannot make outbound requests. The tradeoffs show up in deployment and in CSS fidelity: wkhtmltopdf is built on an old WebKit fork, so modern flexbox, grid, and many web fonts render unpredictably, and the project is no longer actively maintained.
| Aspect | wicked_pdf + wkhtmltopdf | PDFPipe API |
|---|---|---|
| Install | System binary per platform, buildpack or Docker layer | None, just an HTTP call |
| Rendering engine | Old WebKit fork | Current browser engine |
| Modern CSS (grid, flexbox) | Partial, often buggy | Full support |
| Web fonts | Flaky | Standard |
| Network dependency | None (local) | Outbound HTTPS |
| Maintenance | wkhtmltopdf archived | Managed service |
Generating in a background job
For large reports or batches, move the call out of the request cycle. Render in an Active Job, attach the result with Active Storage, and email or notify the user when it is ready. The service object above works unchanged inside the job.
# app/jobs/invoice_pdf_job.rb
class InvoicePdfJob < ApplicationJob
queue_as :default
def perform(invoice)
html = ApplicationController.render(
template: "invoices/show",
layout: "pdf",
assigns: { invoice: invoice }
)
pdf = PdfPipe.render(html, format: "A4")
invoice.document.attach(
io: StringIO.new(pdf),
filename: "invoice-#{invoice.number}.pdf",
content_type: "application/pdf"
)
end
endNotes on reliability
Set a sensible timeout (PDF rendering for a complex page can take a few seconds), handle non 2xx responses by raising so the job retries, and make asset URLs absolute in your pdf layout so images and stylesheets resolve. Because the call is stateless, you can scale rendering simply by scaling requests.
Paste your invoice HTML into the live playground on the home page to preview the output, then read the full request and option reference in the docs.
See pricing →