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:
| Parameter | Type | Default | Description |
|---|---|---|---|
url | string | The webpage to capture | |
format | string | png | Output: png, jpeg, webp, avif, pdf |
width | integer | 1280 | Viewport width in pixels |
height | integer | 720 | Viewport height in pixels |
full_page | boolean | false | Capture the full scrollable page |
device | string | desktop | Device type: desktop, mobile, tablet |
retina | boolean | false | 2x pixel density |
dark_mode | boolean | false | Force dark color scheme |
selector | string | CSS selector to capture a specific element | |
delay | integer | 0 | Wait N milliseconds before capturing |
block_cookies | boolean | false | Remove cookie consent banners |
block_ads | boolean | false | Remove advertisements |
block_chats | boolean | false | Remove live chat widgets |
css | string | Inject custom CSS before capture | |
js | string | Execute 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 chromedp | ScreenshotRun API | |
|---|---|---|
| Setup | go get + install Chrome/Chromium | One HTTP GET request |
| Dependencies | Chrome binary on every server | None (standard library) |
| RAM per capture | 200-400 MB per Chrome instance | Zero on your server |
| Scaling | Browser pools, process limits, OOM monitoring | Managed by the API |
| Cookie banners | Per-site selectors, ongoing maintenance | One parameter: block_cookies=true |
| Font rendering | Install font packages on every Linux server | Consistent across all captures |
| Mobile emulation | Manual width/height/DPR/UA string | device=mobile |
| Concurrency | Goroutines + Chrome process management | Goroutines + HTTP calls |
| Cost | Free (+ 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