Website Screenshot API in Go: Capture Web Pages with Golang

February 22, 2026 · 10 min read

Go is the language of choice for building high-performance backend services, CLI tools, and infrastructure. If you're building something in Go that needs to capture website screenshots - whether that's a monitoring dashboard, a link preview service, or a reporting pipeline - this guide covers everything you need.

We'll use the GrabShot screenshot API with Go's standard library (no external dependencies required) and build up to production-ready patterns including concurrency, retries, and batch processing.

Quick Start: Your First Screenshot in Go

Before diving into Go, here's the equivalent curl command so you can verify your API key works:

curl "https://grabshot.dev/api/screenshot?url=https://example.com&format=png" \
  -H "X-API-Key: YOUR_API_KEY" \
  -o screenshot.png

Now the same thing in Go using only the standard library:

package main

import (
    "fmt"
    "io"
    "net/http"
    "net/url"
    "os"
)

func main() {
    apiKey := os.Getenv("GRABSHOT_API_KEY")

    params := url.Values{}
    params.Set("url", "https://example.com")
    params.Set("format", "png")
    params.Set("width", "1280")
    params.Set("height", "720")

    req, err := http.NewRequest("GET",
        "https://grabshot.dev/api/screenshot?"+params.Encode(), nil)
    if err != nil {
        fmt.Fprintf(os.Stderr, "request error: %v\n", err)
        os.Exit(1)
    }
    req.Header.Set("X-API-Key", apiKey)

    resp, err := http.DefaultClient.Do(req)
    if err != nil {
        fmt.Fprintf(os.Stderr, "fetch error: %v\n", err)
        os.Exit(1)
    }
    defer resp.Body.Close()

    if resp.StatusCode != http.StatusOK {
        body, _ := io.ReadAll(resp.Body)
        fmt.Fprintf(os.Stderr, "API error %d: %s\n", resp.StatusCode, body)
        os.Exit(1)
    }

    f, err := os.Create("screenshot.png")
    if err != nil {
        fmt.Fprintf(os.Stderr, "file error: %v\n", err)
        os.Exit(1)
    }
    defer f.Close()

    written, err := io.Copy(f, resp.Body)
    if err != nil {
        fmt.Fprintf(os.Stderr, "write error: %v\n", err)
        os.Exit(1)
    }
    fmt.Printf("Saved screenshot.png (%d bytes)\n", written)
}

Zero dependencies. That's the Go way.

Building a Reusable Screenshot Client

For anything beyond a one-off script, you'll want a proper client struct with configuration, timeouts, and error handling:

package grabshot

import (
    "context"
    "fmt"
    "io"
    "net/http"
    "net/url"
    "time"
)

type Client struct {
    apiKey     string
    baseURL    string
    httpClient *http.Client
}

type ScreenshotOptions struct {
    URL       string
    Format    string // "png", "jpeg", "webp"
    Width     int
    Height    int
    FullPage  bool
    Delay     int    // milliseconds to wait before capture
    UserAgent string
}

func NewClient(apiKey string) *Client {
    return &Client{
        apiKey:  apiKey,
        baseURL: "https://grabshot.dev/api",
        httpClient: &http.Client{
            Timeout: 30 * time.Second,
        },
    }
}

func (c *Client) Screenshot(ctx context.Context, opts ScreenshotOptions) ([]byte, error) {
    params := url.Values{}
    params.Set("url", opts.URL)

    if opts.Format != "" {
        params.Set("format", opts.Format)
    }
    if opts.Width > 0 {
        params.Set("width", fmt.Sprintf("%d", opts.Width))
    }
    if opts.Height > 0 {
        params.Set("height", fmt.Sprintf("%d", opts.Height))
    }
    if opts.FullPage {
        params.Set("full_page", "true")
    }
    if opts.Delay > 0 {
        params.Set("delay", fmt.Sprintf("%d", opts.Delay))
    }

    req, err := http.NewRequestWithContext(ctx, "GET",
        c.baseURL+"/screenshot?"+params.Encode(), nil)
    if err != nil {
        return nil, fmt.Errorf("creating request: %w", err)
    }
    req.Header.Set("X-API-Key", c.apiKey)

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

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

    return io.ReadAll(resp.Body)
}

Usage is clean:

client := grabshot.NewClient(os.Getenv("GRABSHOT_API_KEY"))

img, err := client.Screenshot(context.Background(), grabshot.ScreenshotOptions{
    URL:    "https://github.com",
    Format: "webp",
    Width:  1440,
})
if err != nil {
    log.Fatal(err)
}
os.WriteFile("github.webp", img, 0644)

Concurrent Batch Screenshots

This is where Go really shines. Need to screenshot 100 URLs? Goroutines and channels make it straightforward. Here's a worker pool pattern with rate limiting:

package main

import (
    "context"
    "fmt"
    "os"
    "sync"
    "time"
)

type Result struct {
    URL   string
    Data  []byte
    Error error
}

func BatchScreenshots(client *grabshot.Client, urls []string, concurrency int) []Result {
    var (
        wg      sync.WaitGroup
        results = make([]Result, len(urls))
        sem     = make(chan struct{}, concurrency)
        limiter = time.NewTicker(200 * time.Millisecond) // 5 req/sec
    )
    defer limiter.Stop()

    for i, u := range urls {
        wg.Add(1)
        go func(idx int, targetURL string) {
            defer wg.Done()
            sem <- struct{}{}        // acquire slot
            defer func() { <-sem }() // release slot
            <-limiter.C             // rate limit

            ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
            defer cancel()

            data, err := client.Screenshot(ctx, grabshot.ScreenshotOptions{
                URL:    targetURL,
                Format: "png",
                Width:  1280,
            })

            results[idx] = Result{URL: targetURL, Data: data, Error: err}

            if err != nil {
                fmt.Fprintf(os.Stderr, "[%d] FAIL %s: %v\n", idx, targetURL, err)
            } else {
                filename := fmt.Sprintf("screenshots/%03d.png", idx)
                os.WriteFile(filename, data, 0644)
                fmt.Printf("[%d] OK %s (%d bytes)\n", idx, targetURL, len(data))
            }
        }(i, u)
    }

    wg.Wait()
    return results
}

func main() {
    client := grabshot.NewClient(os.Getenv("GRABSHOT_API_KEY"))
    os.MkdirAll("screenshots", 0755)

    urls := []string{
        "https://github.com",
        "https://golang.org",
        "https://news.ycombinator.com",
        "https://dev.to",
        "https://stackoverflow.com",
    }

    results := BatchScreenshots(client, urls, 3) // 3 concurrent workers

    success := 0
    for _, r := range results {
        if r.Error == nil {
            success++
        }
    }
    fmt.Printf("\nDone: %d/%d successful\n", success, len(results))
}

Three goroutines running in parallel, rate-limited to 5 requests per second. Adjust concurrency based on your GrabShot plan limits.

Adding Retry Logic

Network requests fail. APIs have transient errors. Here's a retry wrapper with exponential backoff:

func (c *Client) ScreenshotWithRetry(ctx context.Context, opts ScreenshotOptions, maxRetries int) ([]byte, error) {
    var lastErr error

    for attempt := 0; attempt <= maxRetries; attempt++ {
        if attempt > 0 {
            backoff := time.Duration(1<<uint(attempt-1)) * time.Second
            select {
            case <-time.After(backoff):
            case <-ctx.Done():
                return nil, ctx.Err()
            }
        }

        data, err := c.Screenshot(ctx, opts)
        if err == nil {
            return data, nil
        }
        lastErr = err
    }

    return nil, fmt.Errorf("failed after %d retries: %w", maxRetries, lastErr)
}

First retry waits 1 second, second waits 2 seconds, third waits 4 seconds. Context cancellation is respected at each step, so you won't get stuck in a retry loop if the parent context times out.

Real-World Use Case: Monitoring Dashboard

A common Go pattern is a background service that periodically screenshots a list of URLs and stores the results. Here's the core loop:

func MonitorLoop(client *grabshot.Client, urls []string, interval time.Duration) {
    ticker := time.NewTicker(interval)
    defer ticker.Stop()

    // Run immediately on start
    captureAll(client, urls)

    for range ticker.C {
        captureAll(client, urls)
    }
}

func captureAll(client *grabshot.Client, urls []string) {
    timestamp := time.Now().Format("2006-01-02_15-04-05")
    dir := fmt.Sprintf("captures/%s", timestamp)
    os.MkdirAll(dir, 0755)

    for i, u := range urls {
        ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
        data, err := client.ScreenshotWithRetry(ctx, grabshot.ScreenshotOptions{
            URL:      u,
            Format:   "png",
            Width:    1440,
            FullPage: true,
        }, 2)
        cancel()

        if err != nil {
            log.Printf("FAIL %s: %v", u, err)
            continue
        }

        path := fmt.Sprintf("%s/%03d.png", dir, i)
        os.WriteFile(path, data, 0644)
        log.Printf("OK %s -> %s", u, path)
    }
}

Pair this with a simple HTTP server to view the captures, and you've got a lightweight monitoring tool. No Puppeteer installation, no Chrome dependency - just HTTP calls to GrabShot.

Python and Node.js Equivalents

For reference, here are minimal examples in other languages. The same API, different syntax:

Node.js

const res = await fetch(
  `https://grabshot.dev/api/screenshot?url=${encodeURIComponent(url)}&format=png`,
  { headers: { 'X-API-Key': process.env.GRABSHOT_API_KEY } }
);
const buffer = Buffer.from(await res.arrayBuffer());
fs.writeFileSync('screenshot.png', buffer);

Python

import requests, os

r = requests.get("https://grabshot.dev/api/screenshot", params={
    "url": "https://example.com", "format": "png"
}, headers={"X-API-Key": os.environ["GRABSHOT_API_KEY"]})

with open("screenshot.png", "wb") as f:
    f.write(r.content)

API Parameters Reference

ParameterTypeDescription
urlstringTarget URL to screenshot (required)
formatstringpng, jpeg, or webp
widthintViewport width in pixels (default: 1280)
heightintViewport height in pixels (default: 720)
full_pageboolCapture entire scrollable page
delayintWait N ms before capture (for dynamic content)
qualityintJPEG/WebP quality 1-100

See the full API documentation for all available options including device emulation, custom CSS injection, and cookie handling.

Start Capturing Screenshots in Go

Free tier includes 25 screenshots per month. No credit card required.

Get Your Free API Key

Tips for Production

Wrapping Up

Go's standard library gives you everything you need to work with a screenshot API: HTTP client, URL encoding, file I/O, and goroutines for concurrency. No third-party packages required.

The patterns in this guide work for any volume: from a CLI tool that captures one screenshot to a monitoring service processing thousands per hour. Start with the free tier and scale up as needed.