---
title: "HTML to Image API for Ruby & Ruby on Rails | Official Gem"
description: "Render HTML to PNG, screenshot URLs and export PDFs from Ruby and Rails. Official zero-dependency gem with a Railtie, Active Job patterns and a bundled CLI."
url: "https://html2img.com/integrations/ruby/"
---

# HTML to Image API for Ruby and Ruby on Rails

The official Ruby gem is built on Net::HTTP from the standard library, so it adds no runtime dependencies. In a Rails application a Railtie wires it up for you, and an install generator writes the initializer.

- **Install:** `bundle add html2img-client`
- **Registry:** [RubyGems](https://rubygems.org/gems/html2img-client)
- **Source:** https://github.com/html2img/html2img-ruby
- **Licence:** MIT
- **Requires:** Ruby 3.1 or newer

One gem covers both plain Ruby and Rails. It is built on Net::HTTP from the
standard library, so it adds no runtime dependencies to your `Gemfile.lock`. In a
Rails application a Railtie wires it up before your initializers run, an install
generator writes the config, and the response drops straight into an Active
Storage attachment or an ERB view.

## What you can build

- **Open Graph images per record**, rendered from an Action View template on
  publish and stored on the model.
- **Invoices and receipts** as PNGs on screen, and as vector PDFs attached to an
  Action Mailer message via the [HTML to PDF API](https://html2img.com/html-to-pdf/).
- **Certificates, tickets and passes** generated when an order completes.
- **Website screenshots** for link previews, listing thumbnails or a visual
  record, through the [Screenshot API](https://html2img.com/screenshot-api/).
- **Digest images for email**, where a chart drawn in HTML will not render but a
  PNG of it will.

## Requirements

| Requirement | Version |
| --- | --- |
| Ruby | 3.1 or newer (tested on 3.1, 3.2, 3.3, 3.4 and 4.0) |
| Rails | Any version with a Railtie; Rails support is automatic when Rails is present |
| Runtime dependencies | None; the gem uses Net::HTTP from the standard library |
| API key | Free, from your [dashboard](https://app.html2img.com/register) |

The gem is named `html2img-client`; the namespace is `Html2img`.

## Installation

```bash
bundle add html2img-client
```

Or in your `Gemfile`:

```ruby
gem "html2img-client"
```

Bundler requires the gem for you in a Rails application, so the explicit require
is only needed in plain scripts:

```ruby
require "html2img/client"
```

Set the key in the environment. The client reads it automatically:

```dotenv
HTML2IMG_API_KEY=your-api-key
```

> **Warning: Keep the key on the server**
>
> This is a server-side client. A key shipped to a browser, a mobile app or a
> public repository spends your credits for whoever finds it. Issuing and rotating
> keys is covered in the [authentication docs](https://html2img.com/docs/authentication/).

## Quick start

```ruby
require "html2img/client"

client = Html2img::Client.new # reads HTML2IMG_API_KEY from the environment

response = client.html(
  "<h1 style='font: 700 64px system-ui'>Hello from Ruby</h1>",
  width: 1200,
  height: 630,
  dpi: 2
)

puts response.url # => "https://i.html2img.com/abc123def456.png"
```

The API returns a JSON envelope containing the CDN URL of the render rather than
the bytes, so you can store the URL and re-serve it. `to_s` on a response is the
URL, so it drops straight into string interpolation or a view.

## Configuration

Build a client with explicit configuration:

```ruby
client = Html2img::Client.new(
  api_key: "your-api-key",              # default: ENV["HTML2IMG_API_KEY"]
  base_url: "https://app.html2img.com", # default: ENV["HTML2IMG_BASE_URI"], then this
  timeout: 35                           # seconds
)
```

Or configure the process once and use the module-level shortcuts, which is usually
what an application wants:

```ruby
# config/initializers/html2img.rb
Html2img.configure do |config|
  config.api_key = ENV.fetch("HTML2IMG_API_KEY")
  config.timeout = 45
end

Html2img.html(document, width: 1200, height: 630)
Html2img.screenshot("https://example.com")
Html2img.template("invoice-image", invoice_number: "INV-1042")
```

| Variable | Default | Purpose |
| --- | --- | --- |
| `HTML2IMG_API_KEY` | none | Your key, sent as the `X-API-Key` header |
| `HTML2IMG_BASE_URI` | `https://app.html2img.com` | API base URL; you rarely need to change this |

`Html2img.client` is a memoised client built from that configuration;
`Html2img.reset!` forgets both, which is mostly useful in tests. A client is cheap
to build and safe to share between threads, so a memoised one is fine under Puma.

The 35 second default timeout sits just above the API's 30 second synchronous
render budget. For captures likely to exceed it, pass a `webhook_url` rather than
raising the timeout.

## Rails setup

The gem detects Rails and loads a Railtie, so there is nothing to require.
Generate an initializer:

```bash
bin/rails generate html2img:install
```

That writes a commented `config/initializers/html2img.rb` reading your key from
the environment. Or configure it from any environment file instead, which is handy
for per-environment settings:

```ruby
# config/environments/production.rb
config.html2img.api_key = Rails.application.credentials.html2img_api_key
config.html2img.timeout = 45
```

Both routes end at the same place. The Railtie runs before
`config/initializers`, so an explicit `Html2img.configure` block wins if you use
both.

## HTML to image

`POST /api/html` takes a complete HTML document. Inline your CSS in a `style`
block, or reference remote stylesheets and web fonts with `link` tags in the head.

```ruby
response = client.html(
  document,                                          # a complete HTML document
  css: "body { background: #0f172a; color: #fff; }", # injected after load
  width: 1200,
  height: 630,
  dpi: 2                                             # retina: the file is 2400x1260
)

response.url
```

### Rendering a Rails view

`ApplicationController.render` gives you the string, so the card lives with the
rest of your views:

```ruby
# app/models/og_image.rb
class OgImage
  SIZE = { width: 1200, height: 630, dpi: 2 }.freeze

  def self.html_for(post)
    ApplicationController.render(
      template: "og_images/post",
      layout: false,
      assigns: { post: post }
    )
  end

  def self.for(post)
    Html2img.html(html_for(post), **SIZE).url
  end
end
```

```erb
<%# app/views/og_images/post.html.erb %>
<!doctype html>
<html>
  <head>
    <meta charset="utf-8">
    <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter:wght@400;800&display=swap">
    <style>
      * { box-sizing: border-box }
      body {
        margin: 0; width: 1200px; height: 630px; padding: 80px;
        display: flex; flex-direction: column; justify-content: space-between;
        font-family: Inter, system-ui, sans-serif;
        background: linear-gradient(160deg, #0e1521, #16233a); color: #fff;
      }
      h1 { font-size: 64px; line-height: 1.1; margin: 0; font-weight: 800; letter-spacing: -0.02em }
      .meta { font-size: 22px; color: #aeb7c6 }
    </style>
  </head>
  <body>
    <h1><%= @post.title %></h1>
    <p class="meta"><%= @post.author.name %> &middot; <%= l(@post.published_at.to_date, format: :long) %></p>
  </body>
</html>
```

Then output it in your layout:

```erb
<% if @post.og_image_url.present? %>
  <meta property="og:image" content="<%= @post.og_image_url %>">
  <meta property="og:image:width" content="1200">
  <meta property="og:image:height" content="630">
  <meta name="twitter:card" content="summary_large_image">
<% end %>
```

> **Warning: Asset URLs must be publicly reachable**
>
> Chrome runs on our servers, so `asset_path` pointing at `localhost:3000` resolves
> to nothing and renders blank. Use `asset_url` against a public host in production,
> and in development either reference publicly hosted assets, inline them as data
> URIs, or expose the app with a tunnel and set
> `Rails.application.routes.default_url_options[:host]` to it while you iterate.
> Google Fonts always work.

## Website screenshots

`POST /api/screenshot` captures a live, publicly reachable URL.

```ruby
# Viewport capture
client.screenshot("https://example.com", width: 1200, height: 630)

# The whole scroll length of the page
client.screenshot("https://example.com/pricing", fullpage: true)

# One element, cropped to its own bounding box
client.screenshot("https://example.com/pricing", selector: "#plans", width: 1400)

# Hide a cookie banner and a chat widget before capturing
client.screenshot(
  "https://example.com",
  css: ".cookie-banner, .intercom-launcher { display: none !important; }",
  width: 1440,
  height: 900,
  dpi: 2
)

# Wait for content that arrives after load
client.screenshot(
  "https://your-app.example.com/reports/42",
  wait_for_selector: "#chart-rendered",
  ms_delay: 400,
  width: 1440,
  height: 900
)
```

Injected rules usually need `!important`, because the page's own styles win on
specificity. Prefer `wait_for_selector` over `ms_delay` wherever you control the
markup: it returns as soon as the element exists, where a delay always waits the
full duration. Behaviour in full:
[`fullpage`](https://html2img.com/docs/parameters/fullpage/),
[`selector`](https://html2img.com/docs/parameters/selector/),
[`dimensions`](https://html2img.com/docs/parameters/dimensions/),
[`dpi`](https://html2img.com/docs/parameters/dpi/),
[`wait_for_selector`](https://html2img.com/docs/parameters/wait_for_selector/),
[`ms_delay`](https://html2img.com/docs/parameters/ms_delay/).

> **Note: Screenshotting your own app**
>
> A capture is an anonymous request from the public internet, so an authenticated
> route comes back as your sign-in page. Either render the view directly with
> `Html2img.html`, which needs no HTTP round trip, or expose a route with a signed
> token and capture that.

## HTML to PDF

Pass `format: "pdf"` on either render and the result comes back as an A4 portrait
vector PDF: selectable text, embedded fonts and automatic pagination, for the same
single credit.

```ruby
response = client.html(invoice_html, format: "pdf")

# Wide content, such as a data table, can be scaled down to the page width
response = client.html(report_html, format: "pdf", scale_to_fit: true)

client.save(response, "invoices/#{invoice.number}.pdf")
```

`width`, `height`, `dpi`, `fullpage` and `selector` are ignored in PDF mode, since
the page size is fixed. `response.pdf?` tells you what came back. See the
[`format`](https://html2img.com/docs/parameters/format/) and
[`scale_to_fit`](https://html2img.com/docs/parameters/scale-to-fit/) docs and the
[HTML to PDF API](https://html2img.com/html-to-pdf/) overview.

Attaching one to an Action Mailer message:

```ruby
class InvoiceMailer < ApplicationMailer
  def receipt(invoice)
    response = Html2img.html(
      ApplicationController.render(template: "invoices/show", layout: false, assigns: { invoice: invoice }),
      format: "pdf"
    )

    attachments["invoice-#{invoice.number}.pdf"] = Html2img.download(response)

    mail(to: invoice.client.email, subject: "Your invoice")
  end
end
```

## Named templates

`POST /api/v1/templates/{slug}` renders a [named template](https://html2img.com/templates/) from a
data payload, with no markup of your own. Templates output PNG only.

```ruby
response = client.template(
  "invoice-image",
  invoice_number: "INV-2026-0042",
  business_name: "Coastline Coffee Co",
  client_name: "Riverside Bakery",
  total: "£750.00"
)

# A hash works too, when your data is already one
response = client.template("invoice-image", invoice.as_json)
```

Rails applications most often use the
[invoice](https://html2img.com/templates/invoice-image/),
[receipt](https://html2img.com/templates/receipt-image/),
[certificate](https://html2img.com/templates/certificate-of-completion/),
[event ticket](https://html2img.com/templates/event-ticket/) and
[real estate listing](https://html2img.com/templates/real-estate-listing/) templates. Inputs for each
are in the [template reference](https://html2img.com/docs/templates/).

## Active Job and background rendering

A render belongs in a job, especially a full-page capture. The gem's error classes
map cleanly onto Active Job's retry declarations:

```ruby
class GenerateOgImageJob < ApplicationJob
  queue_as :default

  retry_on Html2img::ServerError, Html2img::ConnectionError,
           wait: :polynomially_longer, attempts: 3
  discard_on Html2img::ValidationError

  def perform(post)
    response = Html2img.html(OgImage.html_for(post), width: 1200, height: 630, dpi: 2)

    post.update!(og_image_url: response.url)
  end
end
```

Retrying a `ServerError` or a `ConnectionError` is worthwhile; retrying a
`ValidationError` is not, since the same request will fail the same way.

Enqueue it when the card's inputs change, not on every save, so an unchanged card
never spends a credit:

```ruby
class Post < ApplicationRecord
  OG_FIELDS = %w[title excerpt author_id published_at].freeze

  after_commit :queue_og_image, on: %i[create update]

  private

  def queue_og_image
    return unless (previous_changes.keys & OG_FIELDS).any?

    GenerateOgImageJob.perform_later(self)
  end
end
```

`after_commit` rather than `after_save` matters: a worker can pick the job up
before the transaction lands otherwise.

### Asynchronous delivery

For captures that will not finish inside the 30 second synchronous budget, pass a
`webhook_url`. The API responds immediately with `status: "processing"` and no
URL, then POSTs the finished URL to your endpoint:

```ruby
response = client.screenshot(
  "https://example.com/very-long-report",
  fullpage: true,
  webhook_url: hooks_html2img_url
)

if response.processing?
  Render.create!(render_id: response.id, status: "pending")
end
```

Handle the callback on a route that skips CSRF verification, since it is a
server-to-server POST:

```ruby
class Hooks::Html2imgController < ActionController::API
  def create
    Render.find_by!(render_id: params[:id]).update!(status: "done", url: params[:url])

    head :ok
  end
end
```

The payload shape is in the
[`webhook_url` reference](https://html2img.com/docs/parameters/webhook-url/).

## Saving and Active Storage

`download` gives you the bytes and `save` writes them to a path, creating parent
directories as needed:

```ruby
response = client.html(document, width: 1200, height: 630)

bytes = client.download(response)               # => String (binary)
path  = client.save(response, "og/post-42.png") # => "og/post-42.png"
```

Both accept a URL string as well as a response, so an earlier render can be
re-downloaded:

```ruby
client.save("https://i.html2img.com/abc123.png", "thumbnails/abc123.png")
```

Attaching to Active Storage, which also covers S3 and every other configured
service:

```ruby
class Post < ApplicationRecord
  has_one_attached :og_image
end

response = Html2img.html(OgImage.html_for(post), width: 1200, height: 630, dpi: 2)

post.og_image.attach(
  io: StringIO.new(Html2img.download(response)),
  filename: "og-#{post.id}.png",
  content_type: "image/png"
)
```

> **Tip: Free-tier renders expire**
>
> `response.expires_at` is an ISO 8601 string on the free tier and `nil` on paid
> plans, where renders are hosted permanently. Attach or download promptly if a
> free-tier image needs to outlive that window; upgrading also makes earlier renders
> permanent.

## Error handling

Every request-time failure raises an `Html2img::Error` or a subclass. Rescue that
one type to handle anything. No raw Net::HTTP exception escapes the gem, and
invalid arguments are reported before a request is sent, as a plain
`ArgumentError`.

```ruby
begin
  response = client.html(document, width: 1200, height: 630)
rescue Html2img::ValidationError => e
  # 400 or 422: the request was malformed, retrying will not help
  e.details.each { |field, messages| logger.warn("#{field}: #{messages.join(', ')}") }
rescue Html2img::InsufficientCreditsError => e
  logger.error("html2img out of credits: #{e.credits_remaining} left")
rescue Html2img::RateLimitError => e
  logger.warn("html2img rate limited, retry after #{e.retry_after}")
rescue Html2img::Error => e
  logger.error("html2img failed: #{e.status_code} #{e.error_code} #{e.payload}")
end
```

| Exception | Raised on |
| --- | --- |
| `Html2img::AuthenticationError` | 401, missing or invalid API key |
| `Html2img::InsufficientCreditsError` | 402, no credits remaining; exposes `credits_remaining` |
| `Html2img::NotSubscribedError` | 403, no active subscription |
| `Html2img::NotFoundError` | 404, for example an unknown template slug |
| `Html2img::ValidationError` | 400 or 422, with `details` per field |
| `Html2img::RateLimitError` | 429, rate or quota exceeded; exposes `retry_after` |
| `Html2img::TimeoutError` | 408 or 504, or the local timeout elapsed |
| `Html2img::ServerError` | 5xx, an unexpected renderer error |
| `Html2img::ConnectionError` | The request never reached a response |
| `Html2img::Error` | Base type for all of the above |

Options are checked locally before a request is sent, so a typo or an out-of-range
value raises immediately rather than spending a credit on a rejected render:

```ruby
client.html(document, widht: 1200)
# => ArgumentError: Unknown option(s): widht. Valid options are: css, dpi, format, ...

client.html(document, width: 9000)
# => ArgumentError: The width must be between 1 and 5000, got 9000.
```

Retries are left to you, deliberately: a 5xx or a `ConnectionError` is worth
retrying, a 4xx is not. In Rails, `retry_on` and `discard_on` express exactly
that. The full status-code reference is in the
[getting started guide](https://html2img.com/docs/getting-started/).

## Custom transports

All HTTP goes through a single object responding to `#call`, which is the seam for
retry middleware, proxies, connection pooling and tests. The default is
`Html2img::Transport`, built on Net::HTTP. To use Faraday instead:

```ruby
class FaradayTransport
  def initialize(connection) = @connection = connection

  def call(method:, url:, headers:, body:, timeout:)
    response = @connection.run_request(method.downcase.to_sym, url, body, headers) do |request|
      request.options.timeout = timeout
    end

    [response.status, response.body.to_s]
  end
end

client = Html2img::Client.new(transport: FaradayTransport.new(Faraday.new))
```

The client still sends the `X-API-Key`, `Accept` and `Content-Type` headers, and
still maps every status onto the same typed errors.

## Testing

A transport is the simplest way to keep a suite off the network and off your
credit balance, with no HTTP stubbing library:

```ruby
transport = ->(**) { [200, '{"success": true, "url": "https://i.html2img.com/test.png"}'] }
client = Html2img::Client.new(api_key: "test", transport: transport)

expect(client.html("<h1>Hi</h1>").url).to eq("https://i.html2img.com/test.png")
```

In a Rails suite, set it once and reset afterwards:

```ruby
# spec/support/html2img.rb
RSpec.configure do |config|
  config.before do
    Html2img.configure do |c|
      c.api_key = "test"
      c.transport = ->(**) { [200, '{"success": true, "url": "https://i.html2img.com/test.png"}'] }
    end
  end

  config.after { Html2img.reset! }
end
```

Return a 402 body the same way to exercise the out-of-credits path. To test the
job rather than the render, `have_enqueued_job(GenerateOgImageJob)` is usually
enough.

## Command line

Installing the gem also installs an `html2img` executable:

```bash
html2img test                                              # verify your setup
html2img html card.html --width 1200 --height 630 -o card.png
html2img html - --format pdf -o report.pdf < report.html   # read stdin
html2img screenshot https://example.com --fullpage -o shot.png
html2img screenshot https://example.com --selector "#hero" -o hero.png
html2img template invoice-image --data '{"invoice_number": "INV-1042"}'
```

Every command prints the resulting URL, and `--out/-o` also saves the render
locally. Run `html2img --help` for the full list. `html2img test` performs a real
render and costs one credit; to check a key and balance without spending one, call
[`GET /api/me`](https://html2img.com/docs/account/).

## Without the gem

The API is one POST with a header, so Net::HTTP alone will do:

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

def html2img(path, payload)
  uri = URI("https://app.html2img.com/api/#{path}")

  request = Net::HTTP::Post.new(uri)
  request["Content-Type"] = "application/json"
  request["X-API-Key"] = ENV.fetch("HTML2IMG_API_KEY")
  request.body = payload.to_json

  response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true, read_timeout: 35) do |http|
    http.request(request)
  end

  body = JSON.parse(response.body)

  raise "html2img returned #{response.code}: #{body['message']}" unless response.is_a?(Net::HTTPSuccess)

  body
end

html2img("html", { html: document, width: 1200, height: 630, dpi: 2 })
html2img("screenshot", { url: "https://example.com", fullpage: true })
html2img("html", { html: document, format: "pdf", scale_to_fit: true })
html2img("v1/templates/invoice-image", { invoice_number: "INV-1042", total: "£750.00" })
```

With Faraday, and retries on the failures worth retrying:

```ruby
require "faraday"
require "faraday/retry"

class Html2ImgClient
  def initialize(api_key)
    @conn = Faraday.new(url: "https://app.html2img.com/api/") do |f|
      f.request :retry, max: 3, interval: 0.5, backoff_factor: 2,
                exceptions: [Faraday::ConnectionFailed, Faraday::TimeoutError]
      f.request :json
      f.response :json
      f.headers["X-API-Key"] = api_key
      f.adapter Faraday.default_adapter
    end
  end

  def render_html(html, options = {})
    post("html", { html: html }.merge(options))
  end

  def screenshot(url, options = {})
    post("screenshot", { url: url }.merge(options))
  end

  def render_template(slug, payload)
    post("v1/templates/#{slug}", payload)
  end

  private

  def post(path, payload)
    response = @conn.post(path, payload)
    # body["url"] is nil on error responses, so check the status first
    raise "html2img returned #{response.status}" unless response.success?

    response.body["url"]
  end
end
```

What you give up: local option checks, one error hierarchy instead of raw
`Faraday::Error`, the `download` and `save` helpers, the Railtie and the CLI.

## Package reference

**`Html2img::Client.new(api_key: nil, base_url: nil, timeout: nil, transport: nil)`**

| Method | Signature |
| --- | --- |
| `html` | `html(html, **options) -> RenderResponse` |
| `screenshot` | `screenshot(url, **options) -> RenderResponse` |
| `template` | `template(slug, data = {}, **fields) -> RenderResponse` |
| `download` | `download(image) -> String` |
| `save` | `save(image, path) -> String` |

**Module-level shortcuts.** `Html2img.html`, `Html2img.screenshot`,
`Html2img.template`, `Html2img.download` and `Html2img.save` all delegate to
`Html2img.client`, a memoised client built from `Html2img.configure`.
`Html2img.reset!` clears both.

**Render options.** Shared by both renders: `css`, `width`, `height`, `fullpage`,
`dpi`, `webhook_url`, `ms_delay`, `wait_for_selector`, `format`, `scale_to_fit`.
`screenshot` additionally accepts `selector`. Anything you leave out is omitted
from the request, so the server applies its own default. Ranges and behaviour are
in the [parameter reference](https://html2img.com/docs/parameters/).

**`Html2img::RenderResponse`** is frozen:

| Member | Returns | Meaning |
| --- | --- | --- |
| `success?` | Boolean | Whether the API reported success |
| `id` | String | The render id |
| `url` | String | CDN URL, `nil` while an async job is pending |
| `expires_at` | String | ISO 8601 expiry on the free tier, `nil` on paid plans |
| `credits_remaining` | Integer | Credits left after this call |
| `status` | String | `"processing"` for accepted async jobs |
| `message` | String | Human-readable message, when provided |
| `template` | String | Template slug, on template renders |
| `processing?` | Boolean | Whether the job is still rendering |
| `pdf?` | Boolean | Whether the render came back as a PDF |
| `raw` | Hash | The full decoded JSON payload |

`to_s` is the URL.

**Rails:** `bin/rails generate html2img:install` writes the initializer;
`config.html2img.api_key`, `.base_url`, `.timeout` and `.transport` are available
in any environment file.

## Troubleshooting

**The key is not picked up in production.** `ENV.fetch("HTML2IMG_API_KEY")` reads
the process environment, which under systemd or a container is not your shell's.
In Rails, `Rails.application.credentials` in an environment file is often the
tidier route.

**Images and fonts are missing from the render.** Chrome fetches them over the
public internet, so `localhost:3000`, `*.test` and private addresses are invisible.
Use `asset_url` with a public host, inline small assets as data URIs, or tunnel
your dev server.

**A screenshot of my own app returns the sign-in page.** Captures are anonymous.
Render the view directly with `Html2img.html`, or expose a signed route for the
capture.

**`ArgumentError: Unknown option(s)`.** The gem checks option names locally, which
catches `widht` and `full_page` before they cost a credit. The message lists the
valid names.

**The job never runs.** Check that a worker is running for the queue, and that
`config.active_job.queue_adapter` is not still `:async` or `:inline` in the
environment you are testing.

**Every save spends a credit.** Guard the enqueue on `previous_changes`, as in the
[Active Job section](#active-job-and-background-rendering) above, so only a
meaningful edit triggers a render.

**`Html2img::TimeoutError` on full-page captures.** The render exceeded the 30
second synchronous budget. Send the same request with a `webhook_url` rather than
raising the client timeout.

**`Html2img::ConnectionError` in a container.** Outbound HTTPS to
`app.html2img.com` needs to be allowed and the image needs CA certificates; a slim
base image with an empty certificate store is a common cause.

## FAQ

**Is there a separate Rails gem?**
No, and there does not need to be. `html2img-client` loads a Railtie when Rails is
present, ships an install generator, and its error classes are designed to be used
with `retry_on` and `discard_on`. Everything on this page applies to both plain
Ruby and Rails.

**Which Ruby versions are supported?**
Ruby 3.1 or newer. The gem is tested on 3.1, 3.2, 3.3, 3.4 and 4.0.

**Does it work with Sidekiq, GoodJob or Solid Queue?**
Yes. There is nothing adapter-specific: the job calls the client and writes the
URL back. The Active Job example above works on any backend.

**Can I render an ERB partial rather than a whole template?**
Yes. `ApplicationController.render` accepts `partial:` as well as `template:`, but
the API needs a complete HTML document, so wrap the partial in one before sending
it.

**Is a PDF more expensive than an image?**
No. One render is one credit, whether it comes back as a PNG or as a multi-page
PDF.

**How do I check my balance without spending a credit?**
Call [`GET /api/me`](https://html2img.com/docs/account/). `html2img test` performs a real render and
does cost one.

**Can I use Faraday instead of Net::HTTP?**
Yes, through a [custom transport](#custom-transports), which keeps the typed
errors and local option checks while sharing your application's connection pool.
