Screenshot API for Go.
Shotium is a hosted screenshot API you call from Go with the standard library: no chromedp, no Chrome process to manage, no CGO. Send a URL, get back PNG, JPEG or WebP bytes — with full-page capture, template-based OG images, and HMAC-signed URLs you can embed in public HTML.
How do I take a screenshot in Go?
Standard library only. Sign in, create an API key, and this runs as-is:
package main
import (
"fmt"
"io"
"net/http"
"net/url"
"os"
"time"
)
func main() {
q := url.Values{}
q.Set("url", "https://example.com")
q.Set("format", "png")
req, _ := http.NewRequest("GET", "https://api.shotium.com/v1/screenshot?"+q.Encode(), nil)
req.Header.Set("Authorization", "Bearer "+os.Getenv("SHOTIUM_KEY"))
client := &http.Client{Timeout: 60 * time.Second}
resp, err := client.Do(req)
if err != nil {
panic(err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
panic(fmt.Sprintf("render failed: %d", resp.StatusCode))
}
img, _ := io.ReadAll(resp.Body)
os.WriteFile("shot.png", img, 0o644)
}Full-page screenshots and viewport control
Set full_page to capture the entire scroll height (up to 20,000px). url.Values holds strings only, so every value goes in quoted:
q := url.Values{}
q.Set("url", "https://news.ycombinator.com")
q.Set("full_page", "true")
q.Set("format", "webp")
q.Set("quality", "90")
q.Set("width", "1280")Generate OG images from templates
POST typed parameters into one of five built-in templates and get a finished 1200×630 social card back — no HTML to write:
body, _ := json.Marshal(map[string]any{
"template": "blog",
"params": map[string]string{"title": "Shipping fast without breaking things", "author": "Ada L."},
"format": "png",
})
req, _ := http.NewRequest("POST", "https://api.shotium.com/v1/og-image", bytes.NewReader(body))
req.Header.Set("Authorization", "Bearer "+os.Getenv("SHOTIUM_KEY"))
req.Header.Set("Content-Type", "application/json")How do I embed OG images without exposing my API key?
Sign the query string with your signing secret instead of sending your key. Go's url.QueryEscape is RFC 3986 except for one divergence — spaces become + — so patch that and sort keys byte-order:
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"net/url"
"sort"
"strings"
)
// RFC 3986: QueryEscape, then fix its one divergence (space encodes as +)
func rfc3986(s string) string {
return strings.ReplaceAll(url.QueryEscape(s), "+", "%20")
}
func signedURL(params map[string]string, secret string) string {
keys := make([]string, 0, len(params))
for k := range params {
keys = append(keys, k)
}
sort.Strings(keys)
pairs := make([]string, 0, len(keys))
for _, k := range keys {
pairs = append(pairs, rfc3986(k)+"="+rfc3986(params[k]))
}
canonical := strings.Join(pairs, "&")
mac := hmac.New(sha256.New, []byte(secret))
mac.Write([]byte(canonical))
sig := hex.EncodeToString(mac.Sum(nil))
return "https://api.shotium.com/v1/og-image?" + canonical + "&sig=" + sig
}Error handling
Every error is RFC 9457 problem+json with a stable type field you can branch on. Failed renders are never billed:
if resp.StatusCode != http.StatusOK {
var problem struct {
Type string `json:"type"`
Detail string `json:"detail"`
}
json.NewDecoder(resp.Body).Decode(&problem)
switch problem.Type {
case "rate_limited":
// honor the Retry-After header, then retry
case "render_timeout":
// site took >30s — retry or skip
}
}Parameters at a glance
| Param | Default | Notes |
|---|---|---|
url | required | http(s) URL to render |
width / height | 1280 × 800 | Viewport, up to 3840 × 2160 |
full_page | false | Full scroll height, ≤20,000px |
format | png | png | jpeg | webp |
quality | 80 | 1–100, lossy formats only |
Full reference — auth, OG templates, rate limits, error table — in the docs. Template parameters live on /og-templates.
Frequently asked questions
Do I need chromedp to take screenshots in Go?
No. chromedp needs a Chrome binary next to your binary and a process lifecycle to manage. Shotium runs the browsers server-side — from Go it's one standard-library GET request, no CGO, no external processes.
How do I take a full-page screenshot in Go?
Call q.Set("full_page", "true") — url.Values holds strings only, so there is no bool to convert. The renderer scrolls, waits for lazy-loaded content, and captures up to 20,000px. Tall captures take longer, so keep the client Timeout at 60s and request format=webp.
What timeout should the Go http.Client use?
Set Timeout to 60 seconds. The render itself is capped at 30 seconds server-side (longer returns a render_timeout error), so 60s covers the render cap plus network transfer with room to spare.
How should Go code react to a failed render?
client.Do returns a nil error for HTTP failures, so check resp.StatusCode explicitly before io.ReadAll — otherwise the JSON error lands in your image file. Decode the RFC 9457 problem+json body and switch on Type: rate_limited means honor Retry-After, render_timeout means the target took over 30 seconds. Failed renders are never billed.
Try it on your own pages
Sign-up is GitHub OAuth and comes with 100 free render credits — no card. Failed renders never bill. Plans from $15/month on pricing.
Also available for: Python · Node.js · PHP · Ruby · Java · cURL