How to Generate PDFs from URLs with Go (Golang)

Published April 4, 2026 · 6 min read · URLSnap Team

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.

Why Not Use a Go PDF Library?

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.

Get a Free API Key

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)
}

Convert a URL to PDF

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")
}
📄 Supported formats: A4, Letter, Legal, A3, Tabloid. Default is A4.

Post HTML Directly to Generate a 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")
}

HTTP Handler — Stream PDF to Browser

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)
}

Batch PDF Generation with Goroutines

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()
}
💡 Free plan: 20 req/day. Starter ($9/mo): 500 req/day. Pro ($29/mo): 5,000 req/day. Upgrade at urlsnap.dev/#pricing.

Check Your Daily Quota

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)
}

Error Handling

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
}

Start generating PDFs with Go today

Free tier: 20 PDFs/day. No credit card, no CGo, no wkhtmltopdf binary to install.

Get your free API key →