Need to generate PDFs from web pages or HTML in Go? This tutorial shows you how to convert any URL to a PDF document using the URLSnap API — no CGo, no Chromium binaries, no third-party C libraries. Just Go's standard net/http package.
Most Go PDF libraries (gofpdf, go-wkhtmltopdf) either produce plain documents with no CSS support, or require wkhtmltopdf binaries that are painful to install and maintain. URLSnap renders pages with a real headless Chromium engine — so CSS, fonts, and JavaScript all work perfectly.
package main
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
)
func registerAPIKey(email string) (string, error) {
body, _ := json.Marshal(map[string]string{"email": email})
resp, err := http.Post(
"https://urlsnap.dev/api/register",
"application/json",
bytes.NewReader(body),
)
if err != nil {
return "", err
}
defer resp.Body.Close()
var result struct {
Key string `json:"key"`
Message string `json:"message"`
}
json.NewDecoder(resp.Body).Decode(&result)
return result.Key, nil
}
func main() {
key, err := registerAPIKey("you@example.com")
if err != nil {
panic(err)
}
fmt.Println("API key:", key)
}
package main
import (
"fmt"
"io"
"net/http"
"net/url"
"os"
)
const APIKey = "us_your_key_here"
func urlToPDF(targetURL, outputPath, format string) error {
params := url.Values{}
params.Set("url", targetURL)
if format != "" {
params.Set("format", format) // A4, Letter, Legal, A3, Tabloid
}
req, err := http.NewRequest("GET",
"https://urlsnap.dev/api/pdf?"+params.Encode(), nil)
if err != nil {
return err
}
req.Header.Set("x-api-key", APIKey)
resp, err := http.DefaultClient.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(resp.Body)
return fmt.Errorf("API error %d: %s", resp.StatusCode, body)
}
f, err := os.Create(outputPath)
if err != nil {
return err
}
defer f.Close()
_, err = io.Copy(f, resp.Body)
return err
}
func main() {
// Basic A4 PDF
if err := urlToPDF("https://example.com", "example.pdf", "A4"); err != nil {
fmt.Println("Error:", err)
return
}
fmt.Println("Saved example.pdf")
// Letter-size PDF
if err := urlToPDF("https://go.dev", "golang-site.pdf", "Letter"); err != nil {
fmt.Println("Error:", err)
return
}
fmt.Println("Saved golang-site.pdf")
}
Great for rendering Go templates (html/template) as PDF invoices or reports:
package main
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
)
const APIKey = "us_your_key_here"
func htmlToPDF(htmlContent, outputPath string) error {
body, _ := json.Marshal(map[string]string{
"html": htmlContent,
"format": "A4",
})
req, err := http.NewRequest("POST", "https://urlsnap.dev/api/pdf",
bytes.NewReader(body))
if err != nil {
return err
}
req.Header.Set("x-api-key", APIKey)
req.Header.Set("Content-Type", "application/json")
resp, err := http.DefaultClient.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
errBody, _ := io.ReadAll(resp.Body)
return fmt.Errorf("API error %d: %s", resp.StatusCode, errBody)
}
f, err := os.Create(outputPath)
if err != nil {
return err
}
defer f.Close()
_, err = io.Copy(f, resp.Body)
return err
}
func main() {
invoiceHTML := `<!DOCTYPE html>
<html>
<head>
<style>
body { font-family: sans-serif; padding: 40px; color: #111; }
h1 { color: #6366f1; }
table { width: 100%; border-collapse: collapse; margin-top: 20px; }
th, td { padding: 10px; border-bottom: 1px solid #eee; text-align: left; }
.total { font-weight: bold; }
</style>
</head>
<body>
<h1>Invoice #INV-2026-001</h1>
<p>Bill to: Gopher Corp</p>
<table>
<tr><th>Item</th><th>Amount</th></tr>
<tr><td>API Starter Plan</td><td>$9.00</td></tr>
<tr class="total"><td>Total</td><td>$9.00</td></tr>
</table>
</body>
</html>`
if err := htmlToPDF(invoiceHTML, "invoice.pdf"); err != nil {
fmt.Println("Error:", err)
return
}
fmt.Println("Saved invoice.pdf")
}
Proxy the PDF directly to an HTTP client in a Go web server:
package main
import (
"fmt"
"io"
"net/http"
"net/url"
)
const APIKey = "us_your_key_here"
func pdfHandler(w http.ResponseWriter, r *http.Request) {
targetURL := r.URL.Query().Get("url")
if targetURL == "" {
http.Error(w, "url parameter required", http.StatusBadRequest)
return
}
params := url.Values{}
params.Set("url", targetURL)
params.Set("format", "A4")
req, _ := http.NewRequest("GET",
"https://urlsnap.dev/api/pdf?"+params.Encode(), nil)
req.Header.Set("x-api-key", APIKey)
resp, err := http.DefaultClient.Do(req)
if err != nil {
http.Error(w, "upstream error: "+err.Error(), http.StatusBadGateway)
return
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(resp.Body)
http.Error(w, fmt.Sprintf("API error %d: %s", resp.StatusCode, body),
http.StatusBadGateway)
return
}
w.Header().Set("Content-Type", "application/pdf")
w.Header().Set("Content-Disposition", "attachment; filename=\"page.pdf\"")
io.Copy(w, resp.Body)
}
func main() {
http.HandleFunc("/pdf", pdfHandler)
fmt.Println("Listening on :8080")
http.ListenAndServe(":8080", nil)
}
Generate multiple PDFs concurrently using goroutines and a semaphore to avoid rate-limiting:
package main
import (
"fmt"
"io"
"net/http"
"net/url"
"os"
"sync"
)
const APIKey = "us_your_key_here"
func savePDF(targetURL, outputPath string) error {
params := url.Values{}
params.Set("url", targetURL)
params.Set("format", "A4")
req, _ := http.NewRequest("GET",
"https://urlsnap.dev/api/pdf?"+params.Encode(), nil)
req.Header.Set("x-api-key", APIKey)
resp, err := http.DefaultClient.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(resp.Body)
return fmt.Errorf("error %d: %s", resp.StatusCode, body)
}
f, _ := os.Create(outputPath)
defer f.Close()
_, err = io.Copy(f, resp.Body)
return err
}
func main() {
pages := []struct{ url, out string }{
{"https://example.com", "example.pdf"},
{"https://go.dev", "golang.pdf"},
{"https://urlsnap.dev", "urlsnap.pdf"},
}
var wg sync.WaitGroup
sem := make(chan struct{}, 3) // max 3 concurrent requests
for _, p := range pages {
wg.Add(1)
go func(p struct{ url, out string }) {
defer wg.Done()
sem <- struct{}{}
defer func() { <-sem }()
if err := savePDF(p.url, p.out); err != nil {
fmt.Printf("✗ %s: %v\n", p.url, err)
} else {
fmt.Printf("✓ Saved %s\n", p.out)
}
}(p)
}
wg.Wait()
}
package main
import (
"encoding/json"
"fmt"
"net/http"
)
const APIKey = "us_your_key_here"
type UsageInfo struct {
Plan string `json:"plan"`
RequestsToday int `json:"requests_today"`
DailyLimit int `json:"daily_limit"`
RequestsTotal int `json:"requests_total"`
}
func main() {
req, _ := http.NewRequest("GET", "https://urlsnap.dev/api/me", nil)
req.Header.Set("x-api-key", APIKey)
resp, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
defer resp.Body.Close()
var info UsageInfo
json.NewDecoder(resp.Body).Decode(&info)
fmt.Printf("Plan: %s\nUsed today: %d/%d\nAll-time: %d\n",
info.Plan, info.RequestsToday, info.DailyLimit, info.RequestsTotal)
}
func safePDF(targetURL, outputPath string) error {
req, _ := http.NewRequest("GET",
"https://urlsnap.dev/api/pdf?url="+url.QueryEscape(targetURL)+"&format=A4", nil)
req.Header.Set("x-api-key", APIKey)
resp, err := http.DefaultClient.Do(req)
if err != nil {
return fmt.Errorf("request failed: %w", err)
}
defer resp.Body.Close()
switch resp.StatusCode {
case http.StatusOK:
// success — write file below
case http.StatusTooManyRequests:
return fmt.Errorf("daily limit reached — upgrade at urlsnap.dev/#pricing")
case http.StatusUnauthorized:
return fmt.Errorf("invalid API key")
default:
body, _ := io.ReadAll(resp.Body)
return fmt.Errorf("API error %d: %s", resp.StatusCode, body)
}
f, err := os.Create(outputPath)
if err != nil {
return err
}
defer f.Close()
_, err = io.Copy(f, resp.Body)
return err
}
Free tier: 20 PDFs/day. No credit card, no CGo, no wkhtmltopdf binary to install.
Get your free API key →