HTML to Image in Ruby on Rails

Render HTML to a PNG from Rails using Faraday or Net::HTTP.

Best for: Rails apps generating receipts on payment, weekly digest images, or any Active Job background render. Net::HTTP works without a gem; Faraday gives you middleware for retries and logging.

Setup

First, add your API key to your credentials or environment variables:

# config/credentials.yml.enc
html2img:
  api_key: your-api-key

Or using environment variables:

# .env
HTML2IMG_API_KEY=your-api-key

API Response Format

Every successful request returns a JSON body with a url pointing to the generated image:

{
    "success": true,
    "credits_remaining": 95,
    "id": "abc123",
    "url": "https://i.html2img.com/abc123.png"
}

You can either return the url directly to the client, or download the image from it if you need a local copy.

HTML to Image Example

Here’s a simple example of converting HTML to an image:

require 'net/http'
require 'json'

class ImagesController < ApplicationController
  def generate
    uri = URI('https://app.html2img.com/api/html')
    http = Net::HTTP.new(uri.host, uri.port)
    http.use_ssl = true

    request = Net::HTTP::Post.new(uri)
    request['Content-Type'] = 'application/json'
    request['X-API-Key'] = Rails.application.credentials.html2img[:api_key]

    request.body = {
      html: '<div style="padding: 20px; background: #f0f0f0;">
        <h1>Hello from Rails!</h1>
        <p>Generated at ' + Time.current.to_s + '</p>
      </div>',
      css: 'h1 { color: #2563eb; } p { color: #4b5563; }',
      width: 800,
      height: 600
    }.to_json

    response = http.request(request)
    data = JSON.parse(response.body)

    if response.is_a?(Net::HTTPSuccess) && data['success']
      render json: {
        success: true,
        url: data['url'],
        id: data['id'],
        credits_remaining: data['credits_remaining']
      }
    else
      render json: {
        success: false,
        message: data['message'] || 'Unknown error'
      }, status: response.code
    end
  end
end

Screenshot Example

Taking a screenshot of a webpage:

class ImagesController < ApplicationController
  def screenshot
    uri = URI('https://app.html2img.com/api/screenshot')
    http = Net::HTTP.new(uri.host, uri.port)
    http.use_ssl = true

    request = Net::HTTP::Post.new(uri)
    request['Content-Type'] = 'application/json'
    request['X-API-Key'] = Rails.application.credentials.html2img[:api_key]

    request.body = {
      url: 'https://example.com',
      width: 1200,
      height: 800,
      dpi: 2,
      fullpage: true
    }.to_json

    response = http.request(request)
    data = JSON.parse(response.body)

    if response.is_a?(Net::HTTPSuccess) && data['success']
      render json: {
        success: true,
        url: data['url']
      }
    else
      render json: {
        success: false,
        message: data['message'] || 'Unknown error'
      }, status: response.code
    end
  end
end

Using with ERB Templates

You can also generate images from ERB templates:

class InvoicesController < ApplicationController
  def generate_image
    html = render_to_string(
      template: 'invoices/template',
      layout: false,
      locals: { order: Order.find(params[:id]) }
    )

    uri = URI('https://app.html2img.com/api/html')
    http = Net::HTTP.new(uri.host, uri.port)
    http.use_ssl = true

    request = Net::HTTP::Post.new(uri)
    request['Content-Type'] = 'application/json'
    request['X-API-Key'] = Rails.application.credentials.html2img[:api_key]

    request.body = {
      html: html,
      width: 800,
      height: 600
    }.to_json

    response = http.request(request)
    data = JSON.parse(response.body)

    if response.is_a?(Net::HTTPSuccess) && data['success']
      render json: {
        success: true,
        url: data['url']
      }
    else
      render json: {
        success: false,
        message: data['message'] || 'Unknown error'
      }, status: response.code
    end
  end
end

Using ActiveStorage

If you want to persist the image using ActiveStorage, download it from the returned URL and attach it to your model:

require 'open-uri'

class Order < ApplicationRecord
  has_one_attached :invoice_image
end

class InvoicesController < ApplicationController
  def generate_image
    order = Order.find(params[:id])

    html = render_to_string(
      template: 'invoices/template',
      layout: false,
      locals: { order: order }
    )

    uri = URI('https://app.html2img.com/api/html')
    http = Net::HTTP.new(uri.host, uri.port)
    http.use_ssl = true

    request = Net::HTTP::Post.new(uri)
    request['Content-Type'] = 'application/json'
    request['X-API-Key'] = Rails.application.credentials.html2img[:api_key]

    request.body = {
      html: html,
      width: 800,
      height: 600
    }.to_json

    response = http.request(request)
    data = JSON.parse(response.body)

    if response.is_a?(Net::HTTPSuccess) && data['success']
      # Download the image from the returned URL and attach it
      URI.open(data['url']) do |image|
        order.invoice_image.attach(
          io: image,
          filename: "invoice-#{order.id}.png",
          content_type: 'image/png'
        )
      end

      render json: {
        success: true,
        url: url_for(order.invoice_image)
      }
    else
      render json: {
        success: false,
        message: data['message'] || 'Unknown error'
      }, status: response.code
    end
  end
end

Make sure you have ActiveStorage set up and configured for your Rails application if you want to use the attachment feature.

Routes Configuration

Add these routes to your config/routes.rb:

Rails.application.routes.draw do
  post 'generate_image', to: 'images#generate'
  post 'take_screenshot', to: 'images#screenshot'
  post 'generate_invoice_image', to: 'invoices#generate_image'
end

Handle the API key securely and never commit it to version control.

Common patterns

Faraday with retries

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 = {})
    @conn.post('html', { html: html }.merge(options)).body['url']
  end

  def render_template(slug, payload)
    @conn.post("v1/templates/#{slug}", payload).body['url']
  end
end

Use this client from any Active Job for background renders. Failures retry automatically with exponential backoff.

Templates in Rails

Use a named template when you have structured data and want a tested image. Here is the Invoice Image template:

require 'net/http'
require 'json'

uri = URI('https://app.html2img.com/api/v1/templates/invoice-image')
request = Net::HTTP::Post.new(uri)
request['X-API-Key'] = ENV['HTML2IMG_API_KEY']
request['Content-Type'] = 'application/json'
request.body = {
  invoice_number: 'INV-2026-0042',
  business_name: 'Coastline Coffee Co',
  client_name: 'Riverside Bakery',
  items: [{ description: 'Wholesale beans', quantity: '50', unit_price: '$15.00', amount: '$750.00' }],
  total: '$750.00'
}.to_json

response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(request) }
url = JSON.parse(response.body)['url']

The returned url is hosted on i.html2img.com.