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