html2img for Ruby
The official Ruby client for the HTML to Image API at html2img.com. Turn HTML and CSS into images, capture screenshots of live URLs, render named templates, and export A4 PDFs, all returning a typed response object.
Every render runs in real Chrome, so flexbox, grid, custom properties, web fonts and inline JavaScript behave exactly as they do in the browser. The gem has zero runtime dependencies — it is built on Net::HTTP from the standard library — and works anywhere Ruby does: Rails and Sinatra apps, Sidekiq and Active Job workers, rake tasks and one-off scripts. The full API reference lives in the documentation, with a Ruby guide at html2img.com/integrations/ruby.
Three things this gem does, each with its own worked guide:
- HTML to Image API — render a document you control into a PNG.
- Screenshot API — capture any public URL, full page or cropped to one element.
- HTML to PDF API — the same markup as a vector A4 PDF with selectable text.
Contents
- What you can build
- Requirements
- Installation
- Quick start
- Configuration
- Usage
- Saving renders
- Rails
- Background jobs
- Render options
- The response
- Asynchronous delivery
- Error handling
- Custom transports
- Command line
- Verifying your setup
- Other languages and frameworks
- Development
- Links
What you can build
- Open Graph and social images, generated per page or post. See the Open Graph image template and Twitter/X post template.
- Business documents such as invoices, receipts, event tickets and certificates — as PNGs, or as PDFs through the HTML to PDF API.
- Developer assets such as code screenshots and GitHub social previews.
- URL screenshots through the Screenshot API, full page or cropped to a single element, with CSS injection to hide cookie banners and chat widgets before capture.
Browse the full template library, or try the no-signup browser tools to see the output before you write any code.
Requirements
- Ruby 3.1 or newer (tested on 3.1, 3.2, 3.3, 3.4 and 4.0)
- An html2img API key, issued per account from your dashboard
Every account starts with 50 free credits and no card is needed to get started. Free-tier renders are hosted for seven days; on any paid plan they are hosted permanently, including everything you rendered before upgrading.
Keep your API key on the server. This client is designed for server-side use. Shipping your key in client-side code would let anyone spend your credits.
Installation
bundle add html2img-client
Or in your Gemfile:
gem "html2img-client"
The gem is named html2img-client; the namespace is Html2img:
require "html2img/client"
Bundler requires the gem for you in a Rails app, so the require is only needed in plain scripts.
Set your API key in the environment. The client reads it automatically:
HTML2IMG_API_KEY=your-api-key
See the authentication docs for issuing and rotating keys, and the getting started guide for a tour of the API.
Quick start
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"
Configuration
Pass configuration when you build a client:
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 you want in an application:
# 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", number: 1042)
| Variable | Default | Purpose |
|---|---|---|
HTML2IMG_API_KEY |
none | Your key, sent as the X-API-Key header on every request. |
HTML2IMG_BASE_URI |
https://app.html2img.com |
API base URL. You rarely need to change this. |
The default timeout of 35 seconds sits just over the 30 second synchronous render budget. For captures likely to exceed it, pass a webhook_url on the request rather than raising the timeout — see asynchronous delivery.
A client is cheap to build and safe to share between threads, so a memoised one is fine.
Usage
Every render method returns an Html2img::RenderResponse. Options are keyword arguments, validated locally before anything is sent.
Render HTML
POST /api/html. Send a complete HTML document and get back an image of the rendered result. Inline your CSS in a <style> block, or reference remote stylesheets and web fonts with <link> tags in the document head. This is the HTML to Image API; see the html parameter docs.
response = client.html(
document, # a complete HTML document
css: "body { background: #0f172a; color: #fff; }", # injected after load
width: 1200,
height: 630,
dpi: 2 # retina
)
response.url # => "https://i.html2img.com/abc123def456.png"
Capture a screenshot
POST /api/screenshot. Fetch a public URL in real Chrome and capture it. Use selector to crop to a single element, and css to hide cookie banners or chat widgets before the capture. This is the Screenshot API; see the url parameter docs and the selector docs.
response = client.screenshot(
"https://example.com",
width: 1200,
height: 630,
selector: "#hero",
css: ".cookie-banner, .intercom-launcher { display: none !important; }",
dpi: 2
)
Full-page captures grow to the whole scroll length of the document:
client.screenshot("https://example.com/pricing", fullpage: true)
Generate a PDF
Pass format: "pdf" on either render and the result comes back as an A4 portrait vector PDF instead of a PNG: text stays selectable and searchable, web fonts are embedded, and long content paginates automatically. The API ignores width, height, dpi, fullpage and selector in PDF mode, and the response URL points at a .pdf file. One credit, the same as an image. This is the HTML to PDF API; see the format parameter docs.
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")
Render a template
POST /api/v1/templates/{slug}. Render one of the built-in templates from a data payload, with no markup of your own. The data is validated server-side per template. Templates output PNG only.
response = client.template("invoice-image", number: 1042, amount: "£240.00", due_date: "2026-07-01")
# A hash works too, when your data is already one
response = client.template("invoice-image", invoice.as_json)
Saving renders
The API returns the CDN URL of the render rather than the raw bytes, so you can cache and re-serve it from your own infrastructure. When you would rather keep a copy, download gives you the bytes and save writes them to a path, creating parent directories as needed:
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 you can re-download an earlier render:
client.save("https://i.html2img.com/abc123.png", "thumbnails/abc123.png")
To store somewhere else, hand the bytes to whatever you already use — for example Active Storage:
post.og_image.attach(
io: StringIO.new(client.download(response)),
filename: "og-#{post.id}.png",
content_type: "image/png"
)
Rails
The gem detects Rails and loads a Railtie, so there is nothing to require. Generate an initializer:
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:
# config/environments/production.rb
config.html2img.api_key = Rails.application.credentials.html2img_api_key
config.html2img.timeout = 45
Both routes end up at the same place. The Railtie runs before config/initializers, so an explicit Html2img.configure block wins if you use both.
Render an Action View template into the image, so the card lives with the rest of your views:
class OgImage
def self.for(post)
html = ApplicationController.render(
template: "og_images/post",
layout: false,
assigns: { post: post }
)
Html2img.html(html, width: 1200, height: 630, dpi: 2).url
end
end
Then output it in your layout:
<meta property="og:image" content="<%= @post.og_image_url %>">
Background jobs
Renders are a natural fit for a background job, especially full-page captures:
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)
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. For very large captures, prefer asynchronous delivery over a long-running job.
Render options
Both renders accept the following. Any option you leave out is omitted from the request, so the server applies its own default. The complete reference is in the parameter docs.
| Option | Type | Docs |
|---|---|---|
css |
String | css |
width |
Integer | dimensions (1 to 5000) |
height |
Integer | dimensions (ignored when fullpage) |
fullpage |
Boolean | fullpage |
dpi |
Integer | dpi (1 to 4, use 2 for retina) |
webhook_url |
String | webhook_url |
ms_delay |
Integer | ms_delay (1 to 5000) |
wait_for_selector |
String | wait_for_selector |
format |
String / Symbol | format: "png" (default) or "pdf" |
scale_to_fit |
Boolean | PDF only. Scale wide content down to the page width instead of clipping it. |
screenshot also accepts selector to crop the capture to a single element. html does not, since you control the markup.
Options are checked locally before a request is sent, so a typo or an out-of-range value raises an ArgumentError immediately rather than spending a credit on a rejected render:
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.
Custom fonts are loaded by referencing them with <link> tags in your HTML document head, or by linking a web font from the page you capture. Everything referenced by your markup is fetched by the renderer over the public internet, so localhost URLs will not resolve.
The response
Every render returns a frozen Html2img::RenderResponse:
response.success? # => true
response.id # => "abc123", the render id
response.url # => "https://i.html2img.com/abc123.png"
response.expires_at # => ISO 8601 String, or nil on paid plans
response.credits_remaining # => Integer, credits left after this call
response.status # => "processing" for async jobs
response. # => String or nil
response.template # => the template slug, when applicable
response.processing? # => false
response.pdf? # => false
response.raw # => the full decoded JSON payload
to_s is the URL, so a response drops straight into string interpolation or a view.
Asynchronous delivery
Synchronous requests have a 30 second budget. For captures likely to exceed it, pass a webhook_url. The API responds immediately with status: "processing" and no URL, then POSTs the final URL to your endpoint once rendering finishes. See the webhook_url docs.
response = client.screenshot(
"https://example.com/long-report",
fullpage: true,
webhook_url: hooks_html2img_url
)
if response.processing?
# the final URL will arrive at your webhook, not on this response
end
Error handling
Every request-time failure raises an Html2img::Error or one of its subclasses. Rescue that single type to handle any error, or rescue a specific subclass. No raw Net::HTTP exception escapes the gem. Invalid arguments are reported before any request is sent, as a plain ArgumentError.
begin
response = client.html(document)
rescue Html2img::ValidationError => e
# 400 or 422: inspect the per-field messages
e.details.each { |field, | logger.warn("#{field}: #{.join(', ')}") }
rescue Html2img::InsufficientCreditsError => e
logger.error("Out of credits: #{e.credits_remaining}")
rescue Html2img::Error => e
e.status_code # => Integer or nil
e.error_code # => String or nil, the API "code" field
e.payload # => Hash, the decoded body
end
| Exception | When |
|---|---|
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. |
Retries are left to you, so that a retry policy fits your application rather than the other way round. A 5xx or a ConnectionError is worth retrying; a 4xx is not.
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:
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..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 on every request, and still maps every status onto the same typed errors.
In tests, a transport is the simplest way to avoid the network entirely:
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")
Command line
Installing the gem also installs an html2img executable:
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 '{"number": 1042}'
Every command prints the resulting URL, and --out/-o also saves the render locally. Run html2img --help for the full list.
Verifying your setup
Confirm your key and configuration by rendering a small test image:
html2img test
It prints the resulting image URL and your remaining credits, or a clear error if the key is missing or rejected. The check uses one credit. There is also a testing guide for the API itself.
Other languages and frameworks
The same API has worked guides and official packages for Python, Django, PHP, Laravel, JavaScript and Node.js, React, Vue, WordPress and Statamic.
Development
bundle install
bundle exec rspec # specs, no network and no credits spent
bundle exec rubocop # lint
bundle exec rake # both
Publishing to RubyGems is covered in PUBLISHING.md.
Links
HTML to Image API · Screenshot API · HTML to PDF API · Documentation · Ruby guide · Templates · Tools · Features · Comparisons · Articles · Pricing
Licence
MIT. See LICENSE.
