Back to Blog
Guide February 25, 2026 · 4 min read

How to Take Screenshots and Generate PDFs in Go

Go has no headless browser library. The usual workarounds — shell out to Chrome, run a Python sidecar, use a cgo binding — all add operational weight. Here's the clean alternative: one HTTP POST, binary response, standard library.

Go has no headless browser library. When Go services need to capture screenshots or generate PDFs, the usual approaches are: shell out to Chrome, run a sidecar Python process, or reach for a cgo-wrapped browser binding. All of these add operational complexity to what should be a simple output.

Here's the clean approach: one HTTP POST, binary response, standard library.

Screenshot from a URL

package main

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

func Screenshot(url string) ([]byte, error) {
    payload, _ := json.Marshal(map[string]interface{}{
        "url":          url,
        "fullPage":     true,
        "blockBanners": true,
    })

    req, _ := http.NewRequest("POST", "https://pagebolt.dev/api/v1/screenshot", bytes.NewBuffer(payload))
    req.Header.Set("x-api-key", os.Getenv("PAGEBOLT_API_KEY"))
    req.Header.Set("Content-Type", "application/json")

    resp, err := http.DefaultClient.Do(req)
    if err != nil {
        return nil, err
    }
    defer resp.Body.Close()

    return io.ReadAll(resp.Body)
}

func main() {
    img, err := Screenshot("https://example.com")
    if err != nil {
        panic(err)
    }
    os.WriteFile("screenshot.png", img, 0644)
}

PDF from a URL

func PDF(url string) ([]byte, error) {
    payload, _ := json.Marshal(map[string]interface{}{
        "url":          url,
        "blockBanners": true,
    })

    req, _ := http.NewRequest("POST", "https://pagebolt.dev/api/v1/pdf", bytes.NewBuffer(payload))
    req.Header.Set("x-api-key", os.Getenv("PAGEBOLT_API_KEY"))
    req.Header.Set("Content-Type", "application/json")

    resp, err := http.DefaultClient.Do(req)
    if err != nil {
        return nil, err
    }
    defer resp.Body.Close()

    return io.ReadAll(resp.Body)
}

PDF from HTML

If you're generating documents from templates (invoices, reports), pass html instead of url:

import "html/template"

type Invoice struct {
    ID     string
    Amount string
    Due    string
}

const invoiceTmpl = `<!DOCTYPE html>
<html><head><style>
  body { font-family: system-ui; padding: 40px; }
  .amount { font-size: 32px; font-weight: bold; }
</style></head>
<body>
  <h1>Invoice #{{.ID}}</h1>
  <p>Due: {{.Due}}</p>
  <div class="amount">{{.Amount}}</div>
</body></html>`

func InvoicePDF(inv Invoice) ([]byte, error) {
    var buf bytes.Buffer
    t := template.Must(template.New("invoice").Parse(invoiceTmpl))
    t.Execute(&buf, inv)

    payload, _ := json.Marshal(map[string]interface{}{
        "html": buf.String(),
    })

    req, _ := http.NewRequest("POST", "https://pagebolt.dev/api/v1/pdf", bytes.NewBuffer(payload))
    req.Header.Set("x-api-key", os.Getenv("PAGEBOLT_API_KEY"))
    req.Header.Set("Content-Type", "application/json")

    resp, _ := http.DefaultClient.Do(req)
    defer resp.Body.Close()
    return io.ReadAll(resp.Body)
}

Use in an HTTP handler

func screenshotHandler(w http.ResponseWriter, r *http.Request) {
    targetURL := r.URL.Query().Get("url")
    if targetURL == "" {
        http.Error(w, "url required", http.StatusBadRequest)
        return
    }

    img, err := Screenshot(targetURL)
    if err != nil {
        http.Error(w, "capture failed", http.StatusBadGateway)
        return
    }

    w.Header().Set("Content-Type", "image/png")
    w.Write(img)
}

func main() {
    http.HandleFunc("/screenshot", screenshotHandler)
    http.ListenAndServe(":8080", nil)
}

Record a narrated video

The same pattern extends to video recording — useful for Go services that generate docs, demos, or changelogs:

type Step struct {
    Action   string `json:"action"`
    URL      string `json:"url,omitempty"`
    Selector string `json:"selector,omitempty"`
    Note     string `json:"note,omitempty"`
}

type AudioGuide struct {
    Enabled bool   `json:"enabled"`
    Voice   string `json:"voice"`
    Script  string `json:"script"`
}

type VideoRequest struct {
    Steps      []Step     `json:"steps"`
    AudioGuide AudioGuide `json:"audioGuide"`
    Pace       string     `json:"pace"`
}

func RecordVideo() ([]byte, error) {
    body := VideoRequest{
        Steps: []Step{
            {Action: "navigate", URL: "https://yourapp.com", Note: "Open the app"},
            {Action: "click", Selector: "#get-started", Note: "Get started"},
        },
        AudioGuide: AudioGuide{
            Enabled: true,
            Voice:   "nova",
            Script:  "Here's the app. {{1}} {{2}} One click to begin.",
        },
        Pace: "slow",
    }

    payload, _ := json.Marshal(body)
    req, _ := http.NewRequest("POST", "https://pagebolt.dev/api/v1/video", bytes.NewBuffer(payload))
    req.Header.Set("x-api-key", os.Getenv("PAGEBOLT_API_KEY"))
    req.Header.Set("Content-Type", "application/json")

    resp, _ := http.DefaultClient.Do(req)
    defer resp.Body.Close()
    return io.ReadAll(resp.Body)
}

No CGo, no subprocess, no browser binary. Pure Go, standard library HTTP client.

Get Started Free

100 requests/month, no credit card

Screenshots, PDFs, and video recordings from your Go service — no Chrome binary, no CGo, no sidecar process.

Get Your Free API Key →