Features Full Page Screenshot Wait for Selector & Delay Block Cookie Banners Custom Viewport & Device Website to PDF HTML to Image Dark Mode Image Format & Quality MCP Server Pricing Docs Blog Log In Sign Up

Go Screenshot API — Capture Websites with One HTTP Call

chromedp is a powerful library, but running Chrome on your own servers means dealing with 200-400 MB of RAM per browser instance and missing fonts on Linux. The build process gets heavier with every dependency, and if all you need is a URL-to-image conversion, that overhead adds up fast.

ScreenshotRun's Go screenshot API strips the process down to a single GET request using Go's standard net/http package. Pass a URL, receive a PNG, JPEG, WebP, AVIF, or PDF. No Chromium binary and no CGo. The standard library is all you need.

Your first Go screenshot API call in 30 seconds

Create an account at the dashboard to get an API key (free plan, 200 captures per month, no credit card). Then run this:

package main

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

func main() {
	params := url.Values{}
	params.Set("url", "https://github.com")
	params.Set("response_type", "image")

	req, _ := http.NewRequest("GET",
		"https://api.screenshotrun.com/v1/screenshots/capture?"+params.Encode(), nil)
	req.Header.Set("Authorization", "Bearer YOUR_API_KEY")

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

	data, _ := io.ReadAll(resp.Body)
	os.WriteFile("github.png", data, 0644)
	fmt.Println("Saved: github.png")
}

That is the full integration. No browser download and no system-level dependencies. Just the Go standard library talking to an HTTP endpoint. Our infrastructure spins up a real Chrome instance, waits for the page to finish rendering, and streams back the result.

Full example with error handling

The snippet above skips over errors for brevity. A production-grade golang screenshot API integration should validate the HTTP status and handle failures properly:

package main

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

func captureScreenshot(targetURL string, opts map[string]string) ([]byte, error) {
	params := url.Values{}
	params.Set("url", targetURL)
	params.Set("response_type", "image")
	for k, v := range opts {
		params.Set(k, v)
	}

	req, err := http.NewRequest("GET",
		"https://api.screenshotrun.com/v1/screenshots/capture?"+params.Encode(), nil)
	if err != nil {
		return nil, err
	}
	req.Header.Set("Authorization", "Bearer "+os.Getenv("SCREENSHOTRUN_KEY"))

	client := &http.Client{Timeout: 60 * time.Second}
	resp, err := client.Do(req)
	if err != nil {
		return nil, err
	}
	defer resp.Body.Close()

	body, err := io.ReadAll(resp.Body)
	if err != nil {
		return nil, err
	}

	if resp.StatusCode != http.StatusOK {
		return nil, fmt.Errorf("API returned %d: %s", resp.StatusCode, string(body))
	}

	return body, nil
}

func main() {
	screenshot, err := captureScreenshot("https://github.com", map[string]string{
		"format":    "webp",
		"width":     "1920",
		"full_page": "true",
	})
	if err != nil {
		log.Fatal(err)
	}

	os.WriteFile("github-full.webp", screenshot, 0644)
	fmt.Println("Captured full-page WebP screenshot")
}

Store the API key in an environment variable (SCREENSHOTRUN_KEY). This keeps the credential out of your source code and makes rotation painless across environments.

Parallel captures with goroutines

Go's concurrency model fits batch screenshot work naturally. Goroutines and a semaphore channel let you capture multiple URLs at once without drowning the API in requests:

package main

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

func main() {
	apiKey := os.Getenv("SCREENSHOTRUN_KEY")
	urls := []string{
		"https://github.com",
		"https://stackoverflow.com",
		"https://dev.to",
		"https://news.ycombinator.com",
	}

	os.MkdirAll("screenshots", 0755)

	var wg sync.WaitGroup
	sem := make(chan struct{}, 3) // max 3 concurrent

	client := &http.Client{Timeout: 60 * time.Second}

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

			domain := strings.ReplaceAll(
				strings.TrimPrefix(strings.TrimPrefix(targetURL, "https://"), "http://"),
				".", "_",
			)

			params := url.Values{}
			params.Set("url", targetURL)
			params.Set("format", "png")
			params.Set("width", "1280")
			params.Set("response_type", "image")

			req, _ := http.NewRequest("GET",
				"https://api.screenshotrun.com/v1/screenshots/capture?"+params.Encode(), nil)
			req.Header.Set("Authorization", "Bearer "+apiKey)

			resp, err := client.Do(req)
			if err != nil {
				fmt.Printf("FAIL %s: %v\n", domain, err)
				return
			}
			defer resp.Body.Close()

			if resp.StatusCode == http.StatusOK {
				data, _ := io.ReadAll(resp.Body)
				os.WriteFile(fmt.Sprintf("screenshots/%s.png", domain), data, 0644)
				fmt.Printf("OK   %s\n", domain)
			} else {
				fmt.Printf("FAIL %s: HTTP %d\n", domain, resp.StatusCode)
			}
		}(u)
	}

	wg.Wait()
	fmt.Println("Done.")
}

A semaphore channel limits concurrency to three goroutines at a time, keeping you safely under the API rate limit. On paid plans you can raise this number. Each goroutine shares a single http.Client, so connection pooling works out of the box.

Available parameters

Every parameter goes into the query string of the GET request. The table below covers what you will reach for most often:

ParameterTypeDefaultDescription
urlstringThe webpage to capture
formatstringpngOutput: png, jpeg, webp, avif, pdf
widthinteger1280Viewport width in pixels
heightinteger720Viewport height in pixels
full_pagebooleanfalseCapture the full scrollable page
devicestringdesktopDevice type: desktop, mobile, tablet
retinabooleanfalse2x pixel density
dark_modebooleanfalseForce dark color scheme
selectorstringCSS selector to capture a specific element
delayinteger0Wait N milliseconds before capturing
block_cookiesbooleanfalseRemove cookie consent banners
block_adsbooleanfalseRemove advertisements
block_chatsbooleanfalseRemove live chat widgets
cssstringInject custom CSS before capture
jsstringExecute JavaScript before capture

Full reference in the API documentation.

Dark mode, element capture, and CSS injection

Three capabilities that come up constantly in production Go services.

Dark mode. Set dark_mode to true and the browser applies prefers-color-scheme: dark before rendering. Sites that respect this media query switch to their dark theme automatically:

params := url.Values{}
params.Set("url", "https://github.com")
params.Set("dark_mode", "true")
params.Set("response_type", "image")

Element capture. Pass a CSS selector through the selector parameter and the API crops the output to that element's bounding box:

params := url.Values{}
params.Set("url", "https://github.com/anthropics")
params.Set("selector", ".js-repo-list")
params.Set("response_type", "image")

CSS injection. Hide a sticky header, remove a promo bar, or change fonts before the screenshot fires:

params := url.Values{}
params.Set("url", "https://example.com")
params.Set("css", ".promo-banner { display: none !important; } body { font-family: Inter, sans-serif; }")
params.Set("response_type", "image")

All three combine in a single request. You can capture one element, in dark mode, with custom styles applied, from a single GET call.

Error handling and retries

Network calls fail. Rate limits kick in. A production Go service should retry gracefully:

func captureWithRetry(targetURL string, params url.Values, maxRetries int) ([]byte, error) {
	params.Set("url", targetURL)
	params.Set("response_type", "image")

	client := &http.Client{Timeout: 60 * time.Second}

	for attempt := 0; attempt <= maxRetries; attempt++ {
		req, err := http.NewRequest("GET",
			"https://api.screenshotrun.com/v1/screenshots/capture?"+params.Encode(), nil)
		if err != nil {
			return nil, err
		}
		req.Header.Set("Authorization", "Bearer "+os.Getenv("SCREENSHOTRUN_KEY"))

		resp, err := client.Do(req)
		if err != nil {
			if attempt < maxRetries {
				time.Sleep(time.Duration(attempt+1) * time.Second)
				continue
			}
			return nil, err
		}

		body, _ := io.ReadAll(resp.Body)
		resp.Body.Close()

		if resp.StatusCode == http.StatusOK {
			return body, nil
		}

		if resp.StatusCode == http.StatusTooManyRequests && attempt < maxRetries {
			time.Sleep(5 * time.Second)
			continue
		}

		if resp.StatusCode >= 500 && attempt < maxRetries {
			time.Sleep(time.Duration(attempt+1) * 2 * time.Second)
			continue
		}

		return nil, fmt.Errorf("screenshot API error %d: %s", resp.StatusCode, string(body))
	}

	return nil, fmt.Errorf("max retries exceeded for %s", targetURL)
}

Rate-limit responses (429) trigger a five-second pause before retrying. Server errors (5xx) retry with increasing delays. Client errors like invalid credentials stop immediately because repeating the same request won't change the outcome.

HTTP handler for screenshot proxy

A common pattern in Go web services: the frontend needs screenshots for link previews or thumbnail galleries, but the API key has to stay server-side. A thin handler takes care of it:

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

	params := url.Values{}
	params.Set("url", targetURL)
	params.Set("format", r.URL.Query().Get("format"))
	params.Set("width", r.URL.Query().Get("width"))
	params.Set("response_type", "image")

	if params.Get("format") == "" {
		params.Set("format", "png")
	}
	if params.Get("width") == "" {
		params.Set("width", "1280")
	}

	req, _ := http.NewRequest("GET",
		"https://api.screenshotrun.com/v1/screenshots/capture?"+params.Encode(), nil)
	req.Header.Set("Authorization", "Bearer "+os.Getenv("SCREENSHOTRUN_KEY"))

	client := &http.Client{Timeout: 60 * time.Second}
	resp, err := client.Do(req)
	if err != nil {
		http.Error(w, `{"error":"capture failed"}`, http.StatusBadGateway)
		return
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		w.WriteHeader(resp.StatusCode)
		io.Copy(w, resp.Body)
		return
	}

	w.Header().Set("Content-Type", resp.Header.Get("Content-Type"))
	w.Header().Set("Cache-Control", "public, max-age=3600")
	io.Copy(w, resp.Body)
}

Register it with http.HandleFunc("/api/screenshot", screenshotHandler) and your frontend calls /api/screenshot?url=https://example.com to get the image back. The API key never leaves the server. The Cache-Control header prevents repeated captures of the same URL within the cache window.

Go screenshot API vs. self-hosted chromedp

chromedp is the right tool when you need full browser control for clicking buttons, filling forms, and navigating multi-step flows. But when the job is turning URLs into images without running Chrome yourself, the tradeoffs look different.

Self-hosted chromedpScreenshotRun API
Setupgo get + install Chrome/ChromiumOne HTTP GET request
DependenciesChrome binary on every serverNone (standard library)
RAM per capture200-400 MB per Chrome instanceZero on your server
ScalingBrowser pools, process limits, OOM monitoringManaged by the API
Cookie bannersPer-site selectors, ongoing maintenanceOne parameter: block_cookies=true
Font renderingInstall font packages on every Linux serverConsistent across all captures
Mobile emulationManual width/height/DPR/UA stringdevice=mobile
ConcurrencyGoroutines + Chrome process managementGoroutines + HTTP calls
CostFree (+ server CPU/RAM)Free tier (200/month), then usage-based

If your Go service needs interactive browser automation, keep chromedp. For Go developers looking for a chromedp alternative that skips browser management entirely, a Go screenshot API call means less code and fewer moving parts in production.

Start capturing screenshots from Go

Create a free account, grab your API key, and drop it into any of the examples above. Two hundred screenshots per month on the free plan, no credit card.

Get Your Free API Key