Invalid Date

Generating PDFs in Ruby or Go traditionally means installing wkhtmltopdf or running a headless Chromium instance on your server. That comes with dependency management headaches and high memory usage. With FUNBREW PDF API, you just send an HTTP request and get a PDF back.

This guide covers multiple HTTP clients in both Ruby and Go, with practical examples for HTML rendering, templates, and URL conversion. For Node.js, Python, and PHP examples, see the language quickstart guide.

Setup

1. Create an Account

Sign up for free. You get 30 PDF generations per month at no cost.

2. Get Your API Key

Navigate to the "API Keys" section in your dashboard and generate a key. It starts with sk-.

# Set as an environment variable (recommended)
export FUNBREW_PDF_API_KEY="sk-your-api-key"

For secure key management practices, see the security guide.

3. API Endpoint

POST https://api.pdf.funbrew.cloud/v1/pdf/from-html

Send HTML, get a PDF binary back. Simple request/response model. See the full API reference in the documentation.

Ruby

net/http (Standard Library)

No extra gems needed. Here's a minimal example to generate a PDF.

require 'net/http'
require 'json'
require 'uri'

uri = URI('https://api.pdf.funbrew.cloud/v1/pdf/from-html')

http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = true
http.open_timeout = 10
http.read_timeout = 30

request = Net::HTTP::Post.new(uri)
request['Authorization'] = "Bearer #{ENV['FUNBREW_PDF_API_KEY']}"
request['Content-Type'] = 'application/json'
request.body = {
  html: <<~HTML,
    <html>
    <head>
      <style>
        body { font-family: Arial, sans-serif; padding: 40px; }
        h1 { color: #1a1a1a; border-bottom: 2px solid #3b82f6; padding-bottom: 8px; }
        .date { color: #6b7280; margin-top: 24px; }
      </style>
    </head>
    <body>
      <h1>Report</h1>
      <p>This PDF was generated via the API.</p>
      <p class="date">Generated: #{Time.now.iso8601}</p>
    </body>
    </html>
  HTML
  engine: 'quality',
  format: 'A4'
}.to_json

response = http.request(request)

if response.code == '200'
  File.binwrite('output.pdf', response.body)
  puts "PDF generated: output.pdf (#{response.body.bytesize} bytes)"
else
  puts "Error: #{response.code} - #{response.body}"
end

Faraday

Faraday makes it easy to add retries and middleware.

require 'faraday'
require 'json'

conn = Faraday.new(url: 'https://api.pdf.funbrew.cloud') do |f|
  f.request :retry, max: 3, interval: 1, backoff_factor: 2
  f.options.timeout = 30
  f.options.open_timeout = 10
end

response = conn.post('/v1/pdf/from-html') do |req|
  req.headers['Authorization'] = "Bearer #{ENV['FUNBREW_PDF_API_KEY']}"
  req.headers['Content-Type'] = 'application/json'
  req.body = {
    html: '<h1>Hello PDF</h1><p>Generated with Faraday</p>',
    engine: 'fast',
    format: 'A4'
  }.to_json
end

if response.success?
  File.binwrite('output.pdf', response.body)
  puts "PDF generated: output.pdf"
else
  puts "Error: #{response.status} - #{response.body}"
end

Template-Based PDF Generation

Use registered templates to generate PDFs by passing data. See the template engine guide for how to create templates.

require 'faraday'
require 'json'

conn = Faraday.new(url: 'https://api.pdf.funbrew.cloud') do |f|
  f.options.timeout = 30
end

response = conn.post('/v1/pdf/from-template') do |req|
  req.headers['Authorization'] = "Bearer #{ENV['FUNBREW_PDF_API_KEY']}"
  req.headers['Content-Type'] = 'application/json'
  req.body = {
    template_id: 'invoice-v1',
    data: {
      company_name: 'Acme Corp',
      invoice_number: 'INV-2026-0042',
      items: [
        { name: 'Web Consulting', quantity: 1, price: 1500 },
        { name: 'Design Work', quantity: 3, price: 500 }
      ],
      total: 3000
    },
    format: 'A4'
  }.to_json
end

File.binwrite('invoice.pdf', response.body) if response.success?

For more on invoice automation, see the invoice PDF automation guide.

URL to PDF Conversion

Convert existing web pages directly to PDF.

require 'faraday'
require 'json'

conn = Faraday.new(url: 'https://api.pdf.funbrew.cloud') do |f|
  f.options.timeout = 60  # Longer timeout for URL conversion (page loading)
end

response = conn.post('/v1/pdf/from-url') do |req|
  req.headers['Authorization'] = "Bearer #{ENV['FUNBREW_PDF_API_KEY']}"
  req.headers['Content-Type'] = 'application/json'
  req.body = {
    url: 'https://example.com/report',
    format: 'A4',
    wait_for: 'networkidle'  # Wait for JS rendering to complete
  }.to_json
end

File.binwrite('page.pdf', response.body) if response.success?

Go

net/http (Standard Library)

Generate PDFs with Go's standard library. Zero external dependencies.

package main

import (
	"bytes"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"os"
	"time"
)

type PDFRequest struct {
	HTML   string `json:"html"`
	Engine string `json:"engine"`
	Format string `json:"format"`
}

func generatePDF() error {
	payload := PDFRequest{
		HTML: `<html>
<head>
  <style>
    body { font-family: Arial, sans-serif; padding: 40px; }
    h1 { color: #1a1a1a; border-bottom: 2px solid #3b82f6; padding-bottom: 8px; }
  </style>
</head>
<body>
  <h1>Report</h1>
  <p>This PDF was generated from Go via the API.</p>
</body>
</html>`,
		Engine: "quality",
		Format: "A4",
	}

	body, err := json.Marshal(payload)
	if err != nil {
		return fmt.Errorf("marshal error: %w", err)
	}

	req, err := http.NewRequest("POST", "https://api.pdf.funbrew.cloud/v1/pdf/from-html", bytes.NewReader(body))
	if err != nil {
		return fmt.Errorf("request error: %w", err)
	}

	req.Header.Set("Authorization", "Bearer "+os.Getenv("FUNBREW_PDF_API_KEY"))
	req.Header.Set("Content-Type", "application/json")

	client := &http.Client{Timeout: 30 * time.Second}
	resp, err := client.Do(req)
	if err != nil {
		return fmt.Errorf("request failed: %w", err)
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		errBody, _ := io.ReadAll(resp.Body)
		return fmt.Errorf("API error %d: %s", resp.StatusCode, string(errBody))
	}

	pdf, err := io.ReadAll(resp.Body)
	if err != nil {
		return fmt.Errorf("read error: %w", err)
	}

	if err := os.WriteFile("output.pdf", pdf, 0644); err != nil {
		return fmt.Errorf("write error: %w", err)
	}

	fmt.Printf("PDF generated: output.pdf (%d bytes)\n", len(pdf))
	return nil
}

func main() {
	if err := generatePDF(); err != nil {
		fmt.Fprintf(os.Stderr, "Error: %v\n", err)
		os.Exit(1)
	}
}

Resty

Resty simplifies retries and error handling.

package main

import (
	"fmt"
	"os"
	"time"

	"github.com/go-resty/resty/v2"
)

type PDFRequest struct {
	HTML   string `json:"html"`
	Engine string `json:"engine"`
	Format string `json:"format"`
}

func main() {
	client := resty.New().
		SetTimeout(30 * time.Second).
		SetRetryCount(3).
		SetRetryWaitTime(1 * time.Second).
		SetRetryMaxWaitTime(10 * time.Second)

	resp, err := client.R().
		SetHeader("Authorization", "Bearer "+os.Getenv("FUNBREW_PDF_API_KEY")).
		SetHeader("Content-Type", "application/json").
		SetBody(PDFRequest{
			HTML:   "<h1>Hello PDF</h1><p>Generated with Resty</p>",
			Engine: "fast",
			Format: "A4",
		}).
		Post("https://api.pdf.funbrew.cloud/v1/pdf/from-html")

	if err != nil {
		fmt.Fprintf(os.Stderr, "Request failed: %v\n", err)
		os.Exit(1)
	}

	if resp.StatusCode() != 200 {
		fmt.Fprintf(os.Stderr, "API error %d: %s\n", resp.StatusCode(), resp.String())
		os.Exit(1)
	}

	os.WriteFile("output.pdf", resp.Body(), 0644)
	fmt.Printf("PDF generated: output.pdf (%d bytes)\n", len(resp.Body()))
}

Template-Based PDF Generation

package main

import (
	"fmt"
	"os"
	"time"

	"github.com/go-resty/resty/v2"
)

type TemplateRequest struct {
	TemplateID string      `json:"template_id"`
	Data       interface{} `json:"data"`
	Format     string      `json:"format"`
}

type InvoiceData struct {
	CompanyName   string        `json:"company_name"`
	InvoiceNumber string        `json:"invoice_number"`
	Items         []InvoiceItem `json:"items"`
	Total         int           `json:"total"`
}

type InvoiceItem struct {
	Name     string `json:"name"`
	Quantity int    `json:"quantity"`
	Price    int    `json:"price"`
}

func main() {
	client := resty.New().SetTimeout(30 * time.Second)

	resp, err := client.R().
		SetHeader("Authorization", "Bearer "+os.Getenv("FUNBREW_PDF_API_KEY")).
		SetHeader("Content-Type", "application/json").
		SetBody(TemplateRequest{
			TemplateID: "invoice-v1",
			Data: InvoiceData{
				CompanyName:   "Acme Corp",
				InvoiceNumber: "INV-2026-0042",
				Items: []InvoiceItem{
					{Name: "Web Consulting", Quantity: 1, Price: 1500},
					{Name: "Design Work", Quantity: 3, Price: 500},
				},
				Total: 3000,
			},
			Format: "A4",
		}).
		Post("https://api.pdf.funbrew.cloud/v1/pdf/from-template")

	if err != nil {
		fmt.Fprintf(os.Stderr, "Error: %v\n", err)
		os.Exit(1)
	}

	if resp.StatusCode() == 200 {
		os.WriteFile("invoice.pdf", resp.Body(), 0644)
		fmt.Println("Invoice PDF generated")
	}
}

Error Handling

The API returns errors in JSON format. Proper error handling helps you quickly identify and resolve issues. For a comprehensive guide, see the error handling guide.

Ruby

require 'faraday'
require 'json'

class PdfApiError < StandardError
  attr_reader :status, :code, :detail

  def initialize(status, body)
    parsed = JSON.parse(body) rescue {}
    @status = status
    @code = parsed['error'] || 'unknown'
    @detail = parsed['message'] || body
    super("PDF API Error [#{status}] #{@code}: #{@detail}")
  end
end

def generate_pdf_safe(html, options = {})
  conn = Faraday.new(url: 'https://api.pdf.funbrew.cloud') do |f|
    f.request :retry, max: 3,
      retry_statuses: [429, 500, 502, 503],
      interval: 1,
      backoff_factor: 2
    f.options.timeout = options.fetch(:timeout, 30)
  end

  response = conn.post('/v1/pdf/from-html') do |req|
    req.headers['Authorization'] = "Bearer #{ENV['FUNBREW_PDF_API_KEY']}"
    req.headers['Content-Type'] = 'application/json'
    req.body = { html: html, engine: 'quality', format: 'A4' }.to_json
  end

  raise PdfApiError.new(response.status, response.body) unless response.success?

  response.body
rescue Faraday::TimeoutError
  raise PdfApiError.new(408, '{"error":"timeout","message":"Request timed out"}')
rescue Faraday::ConnectionFailed
  raise PdfApiError.new(0, '{"error":"connection","message":"Could not connect to API"}')
end

Go

package pdfapi

import (
	"encoding/json"
	"fmt"
	"io"
	"net/http"
)

type APIError struct {
	StatusCode int
	ErrorCode  string `json:"error"`
	Message    string `json:"message"`
}

func (e *APIError) Error() string {
	return fmt.Sprintf("PDF API Error [%d] %s: %s", e.StatusCode, e.ErrorCode, e.Message)
}

func (e *APIError) IsRetryable() bool {
	switch e.StatusCode {
	case 429, 500, 502, 503:
		return true
	default:
		return false
	}
}

func parseErrorResponse(resp *http.Response) error {
	body, _ := io.ReadAll(resp.Body)
	apiErr := &APIError{StatusCode: resp.StatusCode}
	if err := json.Unmarshal(body, apiErr); err != nil {
		apiErr.ErrorCode = "unknown"
		apiErr.Message = string(body)
	}
	return apiErr
}

Framework Integration

Rails (Ruby on Rails)

Serve PDFs as download responses directly from a Rails controller.

# app/services/pdf_generator.rb
class PdfGenerator
  def initialize
    @conn = Faraday.new(url: 'https://api.pdf.funbrew.cloud') do |f|
      f.request :retry, max: 3, retry_statuses: [429, 500, 502, 503]
      f.options.timeout = 30
    end
  end

  def from_html(html, options = {})
    response = @conn.post('/v1/pdf/from-html') do |req|
      req.headers['Authorization'] = "Bearer #{ENV['FUNBREW_PDF_API_KEY']}"
      req.headers['Content-Type'] = 'application/json'
      req.body = {
        html: html,
        engine: options.fetch(:engine, 'quality'),
        format: options.fetch(:format, 'A4')
      }.to_json
    end

    raise "PDF generation failed: #{response.status}" unless response.success?
    response.body
  end

  def from_template(template_id, data, options = {})
    response = @conn.post('/v1/pdf/from-template') do |req|
      req.headers['Authorization'] = "Bearer #{ENV['FUNBREW_PDF_API_KEY']}"
      req.headers['Content-Type'] = 'application/json'
      req.body = {
        template_id: template_id,
        data: data,
        format: options.fetch(:format, 'A4')
      }.to_json
    end

    raise "PDF generation failed: #{response.status}" unless response.success?
    response.body
  end
end

# app/controllers/invoices_controller.rb
class InvoicesController < ApplicationController
  def download
    invoice = Invoice.find(params[:id])
    generator = PdfGenerator.new

    pdf = generator.from_template('invoice-v1', {
      company_name: invoice.company_name,
      invoice_number: invoice.number,
      items: invoice.items.map { |i| { name: i.name, quantity: i.quantity, price: i.price } },
      total: invoice.total
    })

    send_data pdf,
      filename: "invoice-#{invoice.number}.pdf",
      type: 'application/pdf',
      disposition: 'attachment'
  end
end

Gin (Go)

package main

import (
	"bytes"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"os"
	"time"

	"github.com/gin-gonic/gin"
)

var httpClient = &http.Client{Timeout: 30 * time.Second}

func generatePDFFromHTML(html string) ([]byte, error) {
	body, _ := json.Marshal(map[string]string{
		"html":   html,
		"engine": "quality",
		"format": "A4",
	})

	req, _ := http.NewRequest("POST", "https://api.pdf.funbrew.cloud/v1/pdf/from-html", bytes.NewReader(body))
	req.Header.Set("Authorization", "Bearer "+os.Getenv("FUNBREW_PDF_API_KEY"))
	req.Header.Set("Content-Type", "application/json")

	resp, err := httpClient.Do(req)
	if err != nil {
		return nil, fmt.Errorf("API request failed: %w", err)
	}
	defer resp.Body.Close()

	if resp.StatusCode != 200 {
		errBody, _ := io.ReadAll(resp.Body)
		return nil, fmt.Errorf("API error %d: %s", resp.StatusCode, string(errBody))
	}

	return io.ReadAll(resp.Body)
}

func main() {
	r := gin.Default()

	r.GET("/invoices/:id/pdf", func(c *gin.Context) {
		invoiceID := c.Param("id")

		html := fmt.Sprintf(`<html>
<body>
  <h1>Invoice %s</h1>
  <p>Generated via FUNBREW PDF API</p>
</body>
</html>`, invoiceID)

		pdf, err := generatePDFFromHTML(html)
		if err != nil {
			c.JSON(500, gin.H{"error": err.Error()})
			return
		}

		c.Header("Content-Disposition", fmt.Sprintf(`attachment; filename="invoice-%s.pdf"`, invoiceID))
		c.Data(200, "application/pdf", pdf)
	})

	r.Run(":8080")
}

Echo (Go)

package main

import (
	"fmt"
	"net/http"
	"os"
	"time"

	"github.com/go-resty/resty/v2"
	"github.com/labstack/echo/v4"
)

var restyClient = resty.New().SetTimeout(30 * time.Second)

func generatePDF(html string) ([]byte, error) {
	resp, err := restyClient.R().
		SetHeader("Authorization", "Bearer "+os.Getenv("FUNBREW_PDF_API_KEY")).
		SetHeader("Content-Type", "application/json").
		SetBody(map[string]string{
			"html":   html,
			"engine": "quality",
			"format": "A4",
		}).
		Post("https://api.pdf.funbrew.cloud/v1/pdf/from-html")

	if err != nil {
		return nil, err
	}
	if resp.StatusCode() != 200 {
		return nil, fmt.Errorf("API error %d: %s", resp.StatusCode(), resp.String())
	}
	return resp.Body(), nil
}

func main() {
	e := echo.New()

	e.GET("/reports/:id/pdf", func(c echo.Context) error {
		reportID := c.Param("id")

		html := fmt.Sprintf(`<html>
<body>
  <h1>Report %s</h1>
  <p>Generated via FUNBREW PDF API</p>
</body>
</html>`, reportID)

		pdf, err := generatePDF(html)
		if err != nil {
			return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
		}

		c.Response().Header().Set("Content-Disposition",
			fmt.Sprintf(`attachment; filename="report-%s.pdf"`, reportID))
		return c.Blob(http.StatusOK, "application/pdf", pdf)
	})

	e.Logger.Fatal(e.Start(":8080"))
}

Batch Processing

When generating many PDFs, use concurrency to speed things up. For detailed patterns, see the batch processing guide.

Ruby (Parallel gem)

require 'faraday'
require 'json'
require 'parallel'

conn = Faraday.new(url: 'https://api.pdf.funbrew.cloud') do |f|
  f.request :retry, max: 3, retry_statuses: [429, 500, 502, 503]
  f.options.timeout = 30
end

invoices = [
  { id: 'INV-001', company: 'Acme Corp', total: 1000 },
  { id: 'INV-002', company: 'Globex Inc', total: 2500 },
  { id: 'INV-003', company: 'Initech', total: 1800 },
]

results = Parallel.map(invoices, in_threads: 5) do |invoice|
  html = "<html><body><h1>Invoice #{invoice[:id]}</h1>
          <p>#{invoice[:company]}</p>
          <p>Total: $#{invoice[:total]}</p>
          </body></html>"

  response = conn.post('/v1/pdf/from-html') do |req|
    req.headers['Authorization'] = "Bearer #{ENV['FUNBREW_PDF_API_KEY']}"
    req.headers['Content-Type'] = 'application/json'
    req.body = { html: html, engine: 'fast', format: 'A4' }.to_json
  end

  if response.success?
    File.binwrite("#{invoice[:id]}.pdf", response.body)
    { id: invoice[:id], status: :ok, size: response.body.bytesize }
  else
    { id: invoice[:id], status: :error, code: response.status }
  end
end

results.each { |r| puts "#{r[:id]}: #{r[:status]}" }

Go (goroutines)

package main

import (
	"bytes"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"os"
	"sync"
	"time"
)

type Invoice struct {
	ID      string
	Company string
	Total   int
}

type Result struct {
	ID     string
	Status string
	Size   int
	Err    error
}

func main() {
	invoices := []Invoice{
		{ID: "INV-001", Company: "Acme Corp", Total: 1000},
		{ID: "INV-002", Company: "Globex Inc", Total: 2500},
		{ID: "INV-003", Company: "Initech", Total: 1800},
	}

	client := &http.Client{Timeout: 30 * time.Second}
	results := make([]Result, len(invoices))

	var wg sync.WaitGroup
	sem := make(chan struct{}, 5) // Limit concurrency to 5

	for i, inv := range invoices {
		wg.Add(1)
		go func(idx int, invoice Invoice) {
			defer wg.Done()
			sem <- struct{}{}
			defer func() { <-sem }()

			html := fmt.Sprintf(`<html><body>
				<h1>Invoice %s</h1>
				<p>%s</p>
				<p>Total: $%d</p>
			</body></html>`, invoice.ID, invoice.Company, invoice.Total)

			body, _ := json.Marshal(map[string]string{
				"html": html, "engine": "fast", "format": "A4",
			})

			req, _ := http.NewRequest("POST",
				"https://api.pdf.funbrew.cloud/v1/pdf/from-html",
				bytes.NewReader(body))
			req.Header.Set("Authorization", "Bearer "+os.Getenv("FUNBREW_PDF_API_KEY"))
			req.Header.Set("Content-Type", "application/json")

			resp, err := client.Do(req)
			if err != nil {
				results[idx] = Result{ID: invoice.ID, Status: "error", Err: err}
				return
			}
			defer resp.Body.Close()

			pdf, _ := io.ReadAll(resp.Body)
			if resp.StatusCode == 200 {
				os.WriteFile(invoice.ID+".pdf", pdf, 0644)
				results[idx] = Result{ID: invoice.ID, Status: "ok", Size: len(pdf)}
			} else {
				results[idx] = Result{ID: invoice.ID, Status: "error",
					Err: fmt.Errorf("status %d", resp.StatusCode)}
			}
		}(i, inv)
	}

	wg.Wait()

	for _, r := range results {
		if r.Err != nil {
			fmt.Printf("%s: error - %v\n", r.ID, r.Err)
		} else {
			fmt.Printf("%s: %s (%d bytes)\n", r.ID, r.Status, r.Size)
		}
	}
}

Summary

Both Ruby and Go can generate PDFs with FUNBREW PDF API using simple HTTP requests. The standard libraries work fine, but Faraday (Ruby) and Resty (Go) make retries and timeout handling easier.

Explore these resources to go further:

Powered by FUNBREW PDF