Screenshot API · Go

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

ParamDefaultNotes
urlrequiredhttp(s) URL to render
width / height1280 × 800Viewport, up to 3840 × 2160
full_pagefalseFull scroll height, ≤20,000px
formatpngpng | jpeg | webp
quality801–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