DEV Community

Custodia-Admin
Custodia-Admin

Posted on • Originally published at pagebolt.dev

Screenshot API in Go: Capture Screenshots and PDFs with net/http

Screenshot API in Go: Capture Screenshots and PDFs with net/http

Go services often need to capture web pages: generate invoice PDFs, take screenshots for website monitoring, render HTML reports. The typical path — shelling out to Chrome or embedding a headless browser — adds fat binaries, cgo dependencies, and memory management headaches to a language valued for lean deployments.

A screenshot API avoids all of that. You make a POST request, get back binary image or PDF data, write it to disk or stream it. No Chrome. No cgo. No Docker overlay magic.

This guide shows how to use PageBolt from Go using only the standard library.

Why a Screenshot API in Go Specifically?

Go's biggest advantages — small binaries, fast startup, easy cross-compilation — get undermined the moment you add a headless browser dependency. Chrome's shared libraries are not small. Running Puppeteer from a Go service means spawning a Node.js process. Embedding rod or chromedp works, but you're shipping ~100 MB of Chrome alongside your binary, and your container needs a full Linux desktop environment to run it.

An external screenshot API keeps your Go binary lean. The trade-off is a network call, which for most use cases (async PDF generation, scheduled monitoring, on-demand captures) is not a constraint.

Setup

No dependencies outside the standard library. Set your API key as an environment variable:

export PAGEBOLT_API_KEY=your_api_key_here
Enter fullscreen mode Exit fullscreen mode

Get a free API key at pagebolt.dev — 100 requests/month, no credit card required.

Taking a Screenshot

package main

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

type ScreenshotRequest struct {
    URL          string `json:"url"`
    FullPage     bool   `json:"fullPage,omitempty"`
    BlockBanners bool   `json:"blockBanners,omitempty"`
    BlockAds     bool   `json:"blockAds,omitempty"`
}

func takeScreenshot(targetURL, outputPath string) error {
    payload := ScreenshotRequest{
        URL:          targetURL,
        FullPage:     true,
        BlockBanners: true,
        BlockAds:     true,
    }

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

    req, err := http.NewRequest("POST", "https://pagebolt.dev/api/v1/screenshot", bytes.NewReader(body))
    if err != nil {
        return fmt.Errorf("build request: %w", err)
    }
    req.Header.Set("Content-Type", "application/json")
    req.Header.Set("x-api-key", os.Getenv("PAGEBOLT_API_KEY"))

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

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

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

    return os.WriteFile(outputPath, data, 0644)
}

func main() {
    if err := takeScreenshot("https://example.com", "screenshot.png"); err != nil {
        fmt.Fprintln(os.Stderr, "error:", err)
        os.Exit(1)
    }
    fmt.Println("screenshot saved to screenshot.png")
}
Enter fullscreen mode Exit fullscreen mode

Generating a PDF

The /pdf endpoint accepts either a URL or raw HTML. Useful for invoice generation or any HTML template you control.

package main

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

type PDFRequest struct {
    URL        string     `json:"url,omitempty"`
    HTML       string     `json:"html,omitempty"`
    PDFOptions *PDFOptions `json:"pdfOptions,omitempty"`
}

type PDFOptions struct {
    Format string `json:"format"` // "A4", "Letter", etc.
}

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

    httpReq, err := http.NewRequest("POST", "https://pagebolt.dev/api/v1/pdf", bytes.NewReader(body))
    if err != nil {
        return fmt.Errorf("build request: %w", err)
    }
    httpReq.Header.Set("Content-Type", "application/json")
    httpReq.Header.Set("x-api-key", os.Getenv("PAGEBOLT_API_KEY"))

    resp, err := http.DefaultClient.Do(httpReq)
    if err != nil {
        return fmt.Errorf("execute request: %w", err)
    }
    defer resp.Body.Close()

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

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

    return os.WriteFile(outputPath, data, 0644)
}
Enter fullscreen mode Exit fullscreen mode

Use Case 1: Invoice Generation

Render an HTML invoice template to PDF and write it to disk (or stream it directly to an HTTP response).

func generateInvoice(invoiceID, customerName string, amountCents int) error {
    html := fmt.Sprintf(`<!DOCTYPE html>
<html>
<head><style>
  body { font-family: sans-serif; padding: 40px; }
  .header { font-size: 24px; font-weight: bold; }
  .line { display: flex; justify-content: space-between; margin: 8px 0; }
</style></head>
<body>
  <div class="header">Invoice #%s</div>
  <p>Customer: %s</p>
  <div class="line"><span>Subtotal</span><span>$%.2f</span></div>
  <div class="line"><strong>Total</strong><strong>$%.2f</strong></div>
</body>
</html>`, invoiceID, customerName, float64(amountCents)/100, float64(amountCents)/100)

    return generatePDF(PDFRequest{
        HTML:       html,
        PDFOptions: &PDFOptions{Format: "A4"},
    }, fmt.Sprintf("invoice-%s.pdf", invoiceID))
}
Enter fullscreen mode Exit fullscreen mode

Serving this directly from an HTTP handler is straightforward — read the PDF bytes into memory and write them to the response with Content-Type: application/pdf.

Use Case 2: Website Monitoring

Capture a daily screenshot to detect visual changes — layout breaks, error pages, unexpected content. Compare files by size or use a pixel-diff library.

package main

import (
    "fmt"
    "os"
    "time"
)

func monitorSite(targetURL string) error {
    date := time.Now().Format("2006-01-02")
    outputPath := fmt.Sprintf("snapshots/%s.png", date)

    if err := os.MkdirAll("snapshots", 0755); err != nil {
        return err
    }

    return takeScreenshot(targetURL, outputPath)
}
Enter fullscreen mode Exit fullscreen mode

Run this from a cron job or a Go ticker. Pair it with a simple file-size check — if today's screenshot is dramatically smaller than yesterday's, something likely broke.

Use Case 3: HTML-to-PDF Reports

Generate a weekly report from data your Go service already holds, then render it via the API.

func buildReportHTML(title string, rows []ReportRow) string {
    var sb strings.Builder
    sb.WriteString(fmt.Sprintf(`<!DOCTYPE html><html><head>
<style>table{width:100%%;border-collapse:collapse}
th,td{padding:8px 12px;border:1px solid #ddd;text-align:left}
th{background:#f5f5f5}</style></head>
<body><h1>%s</h1><table><tr><th>Date</th><th>Requests</th><th>Errors</th></tr>`, title))
    for _, r := range rows {
        sb.WriteString(fmt.Sprintf("<tr><td>%s</td><td>%d</td><td>%d</td></tr>",
            r.Date, r.Requests, r.Errors))
    }
    sb.WriteString("</table></body></html>")
    return sb.String()
}

type ReportRow struct {
    Date     string
    Requests int
    Errors   int
}
Enter fullscreen mode Exit fullscreen mode

Pass the returned HTML to generatePDF with HTML field set. The API renders it in a real browser, so CSS layout, flexbox, and grid all work correctly.

Handling the Response as JSON

By default the API returns raw binary. If you need base64-encoded data — for embedding in a JSON response or storing in a database — add response_type: "json":

type ScreenshotJSONRequest struct {
    URL          string `json:"url"`
    ResponseType string `json:"response_type"` // "json"
}

type ScreenshotJSONResponse struct {
    Image    string                 `json:"image"` // base64
    Metadata map[string]interface{} `json:"metadata"`
}
Enter fullscreen mode Exit fullscreen mode

The response shape is { "image": "<base64>", "metadata": { ... } }. Decode with base64.StdEncoding.DecodeString.

Device Emulation

Capture how a page looks on mobile without changing a line of CSS:

type ScreenshotRequest struct {
    URL            string `json:"url"`
    ViewportDevice string `json:"viewportDevice,omitempty"` // e.g. "iphone_14_pro"
}
Enter fullscreen mode Exit fullscreen mode

Get the full list of supported device presets by calling GET /api/v1/devices with your API key.

Free Tier

PageBolt includes 100 requests per month on the free plan — no credit card required. That's enough to integrate both screenshot and PDF endpoints and test every option described above before committing to a paid plan.

Get started: pagebolt.dev

Top comments (0)