# HTML to PDF in Ruby (and Rails)

Convert HTML to PDF in Ruby on Rails with wicked_pdf, Grover, Ferrum, or a Net::HTTP or Faraday call to an API, plus a send_data controller.

Rails apps usually generate PDFs, like invoices, receipts and reports, from an ERB view. The view part is easy. The real choice is the renderer.

You can use the long-standing wkhtmltopdf, a headless Chrome driven from Ruby, or a hosted API. This guide compares them with working code and ends with a controller that needs no browser on your server.

**Quick answer.** POST your HTML to the API and save the bytes. This works from any Ruby app with no gems.

```bash
curl -X POST https://html-pdf.com/v1/pdf \
  -H "Authorization: Bearer $HTML_PDF_KEY" \
  -H "Content-Type: application/json" \
  -d '{"html": "<h1>Invoice #042</h1>"}' \
  -o invoice.pdf
```

## Ruby HTML to PDF options

This table shows what each gem renders with and what it needs on your server.

| Gem | Renders with | Modern CSS | Requires |
|---|---|---|---|
| wicked_pdf | wkhtmltopdf (QtWebKit) | Partial, no grid | wkhtmltopdf binary (archived project) |
| Grover | Puppeteer + Chrome | Yes | Node.js, Puppeteer, Chrome |
| Ferrum | Chrome via DevTools protocol, pure Ruby | Yes | Chrome |
| Net::HTTP / Faraday + html-pdf.com | Headless Chromium, hosted | Yes | Nothing |

## Option 1: wicked_pdf

wicked_pdf has been the default Rails answer for years. It plugs into `respond_to`.

```ruby
# Gemfile
gem "wicked_pdf"
gem "wkhtmltopdf-binary"
```

```ruby
class InvoicesController < ApplicationController
  def show
    @invoice = Invoice.find(params[:id])
    respond_to do |format|
      format.html
      format.pdf do
        render pdf: "invoice-#{@invoice.id}", template: "invoices/show", layout: "pdf",
               page_size: "A4", margin: { top: 15, bottom: 15 }
      end
    end
  end
end
```

The gem still gets releases, but the engine underneath is the problem. wkhtmltopdf's repository was archived in 2023, its WebKit is years old, and flexbox and grid layouts don't render as they do in a browser.

It's fine for existing table-based templates. For anything new, see our [wkhtmltopdf alternative](https://html-pdf.com/wkhtmltopdf-alternative) guide.

## Option 2: Grover (Puppeteer)

Grover calls Puppeteer from Ruby, so you get Chrome's output.

```ruby
# Gemfile
gem "grover"
# and: npm install puppeteer
```

```ruby
html = render_to_string(template: "invoices/show", layout: "pdf", formats: [:html])
pdf = Grover.new(html, format: "A4", print_background: true,
                       margin: { top: "15mm", bottom: "15mm" }).to_pdf
```

The trade-off is that every app server or worker now needs Node.js, Puppeteer and Chrome, and each render starts a browser process.

## Option 3: Ferrum (pure Ruby, Chrome only)

Ferrum talks to Chrome over the DevTools protocol directly. There's no Node.js, but you still need Chrome installed.

```ruby
require "ferrum"

browser = Ferrum::Browser.new(timeout: 30)
page = browser.create_page
page.content = "<div style='display:flex;gap:2rem'><h1>Invoice #042</h1></div>"
pdf = page.pdf(encoding: :binary, format: :A4, print_background: true)
File.binwrite("invoice.pdf", pdf)
browser.quit
```

Ferrum is lean and fast to start. You still own the Chrome install, its memory use under load, and keeping it updated.

## Or skip the server: one HTTP call

html-pdf.com runs headless Chromium for you. You send HTML as JSON and get PDF bytes back, with Grover-quality output and only the standard library.

Get a key, then open the link we email you to confirm your address. That adds 100 free credits.

```bash
curl -X POST https://html-pdf.com/v1/signup -H "Content-Type: application/json" -d '{"email": "you@example.com"}'
```

### With Net::HTTP (no gems)

```ruby
require "net/http"
require "json"

module HtmlPdf
  ENDPOINT = URI("https://html-pdf.com/v1/pdf")
  Error = Class.new(StandardError)

  def self.render(html, **options)
    req = Net::HTTP::Post.new(ENDPOINT)
    req["Authorization"] = "Bearer #{ENV.fetch('HTML_PDF_KEY')}"
    req["Content-Type"] = "application/json"
    req.body = { html: html, format: "A4", margin: "15mm", max_pages: 20 }.merge(options).to_json

    res = Net::HTTP.start(ENDPOINT.host, ENDPOINT.port, use_ssl: ENDPOINT.scheme == "https",
                          read_timeout: 60) { |http| http.request(req) }

    unless res.is_a?(Net::HTTPSuccess)
      err = (JSON.parse(res.body)["error"] rescue nil) || {}
      raise Error, "#{res.code} #{err['code']}: #{err['message']}"
    end
    res.body # binary PDF; res["X-Pages"] holds the page count
  end
end

File.binwrite("invoice.pdf", HtmlPdf.render("<h1>Invoice #042</h1>"))
```

### With Faraday

```ruby
conn = Faraday.new(url: "https://html-pdf.com") do |f|
  f.request :authorization, "Bearer", -> { ENV.fetch("HTML_PDF_KEY") }
  f.request :json
  f.options.timeout = 60
end

res = conn.post("/v1/pdf", { html: html, format: "A4", margin: "15mm" })
raise "PDF failed: #{res.body}" unless res.success?
pdf = res.body
```

To convert a public page instead, pass `url: "https://example.com"` instead of `html`. Add `response: "url"` to get JSON with a download link valid for 24 hours.

## Rails controller with send_data

This controller renders an ERB template, converts it and sends the PDF to the browser.

```ruby
class InvoicePdfsController < ApplicationController
  def show
    invoice = current_user.invoices.find(params[:invoice_id])
    html = render_to_string(template: "invoices/pdf", layout: "pdf",
                            locals: { invoice: invoice }, formats: [:html])

    pdf = HtmlPdf.render(html,
      footer_html: '<div style="font-size:9px;width:100%;text-align:center">' \
                   'Page <span class="pageNumber"></span> of <span class="totalPages"></span></div>')

    send_data pdf, filename: "invoice-#{invoice.id}.pdf",
                   type: "application/pdf", disposition: "inline"
  rescue HtmlPdf::Error => e
    Rails.logger.error(e.message)
    head :bad_gateway
  end
end
```

```ruby
# config/routes.rb
resources :invoices do
  resource :pdf, only: :show, controller: "invoice_pdfs"
end
```

Use `disposition: "attachment"` to force a download. ERB's `<%= %>` escapes output, so customer data can't break the markup.

**Assets.** The renderer can't reach `localhost` or private networks, so `image_tag` and `stylesheet_link_tag` need absolute public URLs (set `config.asset_host`). In development, inline CSS in a `<style>` tag or pass it as `css:`, and use data URIs for images. You can also send the HTML and its files as multipart (`index.html` plus assets, with an `options` JSON field).

For mailers and exports, render inside an Active Job and attach the bytes with Active Storage (`attach(io: StringIO.new(pdf), filename: ..., content_type: "application/pdf")`).

## Credits and errors

Here is what you pay and what can go wrong. 1 credit = 1 PDF page, with 100 free credits and then packs from $10 for 1,000 pages. Purchased credits never expire, failed renders are free, and `max_pages` stops a runaway document without charge.

Errors come back as JSON `{"error": {"code", "message", "hint"}}`. A `402 insufficient_credits` means it's time to top up.

Limits are 20 MB per request, 30 seconds per render and 120 requests per minute.

More: [page breaks](https://html-pdf.com/html-to-pdf-page-breaks) · [headers, footers and page numbers](https://html-pdf.com/pdf-headers-footers-page-numbers) · [HTML to PDF in Python](https://html-pdf.com/html-to-pdf-python) · [API docs](https://html-pdf.com/docs) · [sign in](https://html-pdf.com/login)

## FAQ

### How do I convert HTML to PDF in Ruby on Rails?

Render your view with `render_to_string`, POST the HTML to `https://html-pdf.com/v1/pdf`, and return the bytes with `send_data`. Gems like wicked_pdf, Grover and Ferrum also work if you can run their engines on your server.

### Is wicked_pdf still a good choice?

It works for existing table-based templates. Its wkhtmltopdf engine was archived in 2023 and doesn't handle flexbox or grid like a modern browser.

### Is there a free HTML to PDF API for Ruby?

html-pdf.com gives you 100 free credits once, with no card needed. 1 credit = 1 PDF page, and failed renders cost nothing.

### Do credits expire?

No. Purchased credits never expire. Packs go from $10 for 1,000 pages to $250 for 50,000 pages.

