How to Take Website Screenshots with Go

Published April 3, 2026 · 5 min read · URLSnap Team

Need to capture website screenshots from a Go application? Whether you're building a monitoring tool, a link-preview service, or an archiving pipeline, this tutorial shows you how to go from URL to PNG image with just the Go standard library — no CGo, no browser binaries to install.

We'll use the URLSnap REST API: you send an HTTP request, get back an image. No Chromium process management, no memory leaks to worry about.

Prerequisites

No external packages needed — only the Go standard library (net/http, os, io). Go 1.18+ recommended.

Step 1: Get a Free API Key

curl -X POST https://urlsnap.dev/api/register \
  -H "Content-Type: application/json" \
  -d '{"email":"you@example.com"}'

# {"key":"us_abc123...","message":"Free tier: 20 requests/day"}

Step 2: Take Your First Screenshot

package main

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

const apiKey = "us_your_key_here" // get free at urlsnap.dev

func screenshotURL(targetURL, outputPath string) error {
	params := url.Values{}
	params.Set("url", targetURL)
	params.Set("format", "png")
	params.Set("full_page", "true")
	params.Set("width", "1280")

	req, err := http.NewRequest("GET",
		"https://urlsnap.dev/api/screenshot?"+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() {
	if err := screenshotURL("https://example.com", "screenshot.png"); err != nil {
		fmt.Fprintln(os.Stderr, "Error:", err)
		os.Exit(1)
	}
	fmt.Println("Screenshot saved to screenshot.png")
}
💡 full_page=true captures the entire scrollable page height, not just the visible viewport. Great for long landing pages.

All API Parameters

ParameterTypeDefaultDescription
urlstringrequiredTarget URL to screenshot
widthinteger1280Viewport width in pixels
heightinteger800Viewport height in pixels
formatstringpngpng or jpeg
full_pageboolfalseCapture full scrollable page
delayinteger0Wait ms after load, max 5000

Reusable Client with Options

package urlsnap

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

type Client struct {
	APIKey     string
	HTTPClient *http.Client
}

type ScreenshotOptions struct {
	Width    int
	Height   int
	Format   string // "png" or "jpeg"
	FullPage bool
	DelayMS  int
}

func NewClient(apiKey string) *Client {
	return &Client{
		APIKey:     apiKey,
		HTTPClient: &http.Client{Timeout: 60 * time.Second},
	}
}

func (c *Client) Screenshot(targetURL, outputPath string, opts ScreenshotOptions) error {
	params := url.Values{}
	params.Set("url", targetURL)

	if opts.Width > 0 {
		params.Set("width", strconv.Itoa(opts.Width))
	}
	if opts.Height > 0 {
		params.Set("height", strconv.Itoa(opts.Height))
	}
	if opts.Format != "" {
		params.Set("format", opts.Format)
	}
	if opts.FullPage {
		params.Set("full_page", "true")
	}
	if opts.DelayMS > 0 {
		params.Set("delay", strconv.Itoa(opts.DelayMS))
	}

	req, err := http.NewRequest("GET",
		"https://urlsnap.dev/api/screenshot?"+params.Encode(), nil)
	if err != nil {
		return err
	}
	req.Header.Set("x-api-key", c.APIKey)

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

	if resp.StatusCode == http.StatusTooManyRequests {
		return fmt.Errorf("daily limit reached — upgrade at urlsnap.dev/#pricing")
	}
	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
}

Batch Screenshots with Goroutines

package main

import (
	"fmt"
	"sync"
)

func batchScreenshot(client *urlsnap.Client, urls []string) {
	var wg sync.WaitGroup
	sem := make(chan struct{}, 3) // max 3 concurrent requests

	for i, u := range urls {
		wg.Add(1)
		go func(i int, u string) {
			defer wg.Done()
			sem <- struct{}{}
			defer func() { <-sem }()

			outputPath := fmt.Sprintf("screenshot_%d.png", i)
			opts := urlsnap.ScreenshotOptions{Width: 1280, FullPage: true}
			if err := client.Screenshot(u, outputPath, opts); err != nil {
				fmt.Printf("Error for %s: %v\n", u, err)
				return
			}
			fmt.Printf("Saved %s\n", outputPath)
		}(i, u)
	}
	wg.Wait()
}

func main() {
	client := urlsnap.NewClient("us_your_key_here")
	urls := []string{
		"https://example.com",
		"https://github.com",
		"https://golang.org",
	}
	batchScreenshot(client, urls)
}
⚡ The semaphore pattern (sem := make(chan struct{}, 3)) keeps concurrency bounded, preventing you from burning through your daily quota in a single burst.

Generate a PDF Instead

func (c *Client) PDF(targetURL, outputPath, format string) error {
	params := url.Values{}
	params.Set("url", targetURL)
	params.Set("format", format) // "A4", "Letter", "Legal"

	req, _ := http.NewRequest("GET",
		"https://urlsnap.dev/api/pdf?"+params.Encode(), nil)
	req.Header.Set("x-api-key", c.APIKey)

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

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

	f, _ := os.Create(outputPath)
	defer f.Close()
	_, err = io.Copy(f, resp.Body)
	return err
}

// Usage:
// client.PDF("https://example.com", "page.pdf", "A4")

Check Your Daily Quota

package main

import (
	"encoding/json"
	"fmt"
	"net/http"
)

type UsageInfo struct {
	Plan          string `json:"plan"`
	RequestsToday int    `json:"requests_today"`
	DailyLimit    int    `json:"daily_limit"`
	RequestsTotal int    `json:"requests_total"`
}

func checkUsage(apiKey string) (*UsageInfo, error) {
	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 {
		return nil, err
	}
	defer resp.Body.Close()

	var info UsageInfo
	return &info, json.NewDecoder(resp.Body).Decode(&info)
}

func main() {
	info, err := checkUsage("us_your_key_here")
	if err != nil {
		fmt.Println("Error:", err)
		return
	}
	fmt.Printf("Plan: %s\nUsed today: %d/%d\nAll-time: %d\n",
		info.Plan, info.RequestsToday, info.DailyLimit, info.RequestsTotal)
}

Ready to screenshot the web with Go?

Free tier: 20 screenshots/day. No credit card, no CGo, no browser setup.

Get your free API key →