How to Take Screenshots in Go with the PageBolt API
You're building a Go service that needs screenshots. You reach for json.Unmarshal and... nothing happens. The response is binary PNG data, not JSON.
This is the most common mistake when integrating screenshot APIs in Go.
Here's the right way: treat the response as raw bytes, not JSON.
The Basic Pattern: Binary Response Handling
package main
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
)
// ScreenshotRequest defines the request payload
type ScreenshotRequest struct {
URL string `json:"url"`
Format string `json:"format"` // png, jpeg, webp
Width int `json:"width,omitempty"`
Height int `json:"height,omitempty"`
FullPage bool `json:"fullPage,omitempty"`
BlockBanners bool `json:"blockBanners,omitempty"`
}
func takeScreenshot(url, outputPath, apiKey string) error {
// Build request
reqBody := ScreenshotRequest{
URL: url,
Format: "png",
Width: 1280,
Height: 720,
FullPage: true,
BlockBanners: true,
}
// Marshal to JSON
body, err := json.Marshal(reqBody)
if err != nil {
return fmt.Errorf("marshal request: %w", err)
}
// Create HTTP request
req, err := http.NewRequest(
http.MethodPost,
"https://api.pagebolt.dev/v1/screenshot",
bytes.NewReader(body),
)
if err != nil {
return fmt.Errorf("create request: %w", err)
}
// Set headers
req.Header.Set("Authorization", "Bearer "+apiKey)
req.Header.Set("Content-Type", "application/json")
// Execute request
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return fmt.Errorf("do request: %w", err)
}
defer resp.Body.Close()
// Check status
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("api error: %d", resp.StatusCode)
}
// Read binary response (NOT JSON)
screenshotData, err := io.ReadAll(resp.Body)
if err != nil {
return fmt.Errorf("read response: %w", err)
}
// Write to file
if err := os.WriteFile(outputPath, screenshotData, 0644); err != nil {
return fmt.Errorf("write file: %w", err)
}
return nil
}
func main() {
if err := takeScreenshot(
"https://example.com",
"screenshot.png",
os.Getenv("PAGEBOLT_API_KEY"),
); err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
fmt.Println("Screenshot saved to screenshot.png")
}
Key points:
- Request is JSON:
json.Marshal()the struct - Response is binary PNG: use
io.ReadAll()to read raw bytes -
Do NOT use
json.Unmarshal()on the response - Write with
os.WriteFile()
Structured Client: Reusable Approach
For larger projects, create a client struct:
package screenshot
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
)
// Client wraps the PageBolt API
type Client struct {
apiKey string
client *http.Client
}
// NewClient creates a new screenshot client
func NewClient(apiKey string) *Client {
return &Client{
apiKey: apiKey,
client: &http.Client{},
}
}
// ScreenshotOptions defines screenshot parameters
type ScreenshotOptions struct {
URL string
Format string // png, jpeg, webp
Width int
Height int
FullPage bool
BlockBanners bool
BlockAds bool
DarkMode bool
}
// Request payload
type requestPayload struct {
URL string `json:"url"`
Format string `json:"format"`
Width int `json:"width,omitempty"`
Height int `json:"height,omitempty"`
FullPage bool `json:"fullPage,omitempty"`
BlockBanners bool `json:"blockBanners,omitempty"`
BlockAds bool `json:"blockAds,omitempty"`
DarkMode bool `json:"darkMode,omitempty"`
}
// Take captures a screenshot and writes to file
func (c *Client) Take(opts ScreenshotOptions, outputPath string) error {
payload := requestPayload{
URL: opts.URL,
Format: opts.Format,
Width: opts.Width,
Height: opts.Height,
FullPage: opts.FullPage,
BlockBanners: opts.BlockBanners,
BlockAds: opts.BlockAds,
DarkMode: opts.DarkMode,
}
// Default values
if payload.Format == "" {
payload.Format = "png"
}
if payload.Width == 0 {
payload.Width = 1280
}
if payload.Height == 0 {
payload.Height = 720
}
// Marshal request
body, err := json.Marshal(payload)
if err != nil {
return fmt.Errorf("marshal: %w", err)
}
// Create request
req, err := http.NewRequest(
http.MethodPost,
"https://api.pagebolt.dev/v1/screenshot",
bytes.NewReader(body),
)
if err != nil {
return fmt.Errorf("new request: %w", err)
}
req.Header.Set("Authorization", "Bearer "+c.apiKey)
req.Header.Set("Content-Type", "application/json")
// Execute
resp, err := c.client.Do(req)
if err != nil {
return fmt.Errorf("do: %w", err)
}
defer resp.Body.Close()
// Check status
if resp.StatusCode != http.StatusOK {
data, _ := io.ReadAll(resp.Body)
return fmt.Errorf("api error %d: %s", resp.StatusCode, string(data))
}
// Read binary response
data, err := io.ReadAll(resp.Body)
if err != nil {
return fmt.Errorf("read: %w", err)
}
// Write file
if err := os.WriteFile(outputPath, data, 0644); err != nil {
return fmt.Errorf("write: %w", err)
}
return nil
}
// TakeBytes captures a screenshot and returns the PNG bytes
func (c *Client) TakeBytes(opts ScreenshotOptions) ([]byte, error) {
payload := requestPayload{
URL: opts.URL,
Format: opts.Format,
Width: opts.Width,
Height: opts.Height,
FullPage: opts.FullPage,
BlockBanners: opts.BlockBanners,
BlockAds: opts.BlockAds,
DarkMode: opts.DarkMode,
}
if payload.Format == "" {
payload.Format = "png"
}
if payload.Width == 0 {
payload.Width = 1280
}
if payload.Height == 0 {
payload.Height = 720
}
body, err := json.Marshal(payload)
if err != nil {
return nil, err
}
req, err := http.NewRequest(
http.MethodPost,
"https://api.pagebolt.dev/v1/screenshot",
bytes.NewReader(body),
)
if err != nil {
return nil, err
}
req.Header.Set("Authorization", "Bearer "+c.apiKey)
req.Header.Set("Content-Type", "application/json")
resp, err := c.client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("api error: %d", resp.StatusCode)
}
return io.ReadAll(resp.Body)
}
Usage:
package main
import (
"fmt"
"os"
"yourmodule/screenshot"
)
func main() {
client := screenshot.NewClient(os.Getenv("PAGEBOLT_API_KEY"))
// Single screenshot
err := client.Take(
screenshot.ScreenshotOptions{
URL: "https://example.com",
FullPage: true,
},
"homepage.png",
)
if err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
// Get bytes instead of writing
data, err := client.TakeBytes(screenshot.ScreenshotOptions{
URL: "https://example.com/about",
})
if err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
fmt.Printf("Screenshot: %d bytes\n", len(data))
}
Concurrent Screenshots: goroutines
Capture multiple URLs in parallel:
package main
import (
"fmt"
"os"
"sync"
"yourmodule/screenshot"
)
func captureScreenshots(urls []string, apiKey string) error {
client := screenshot.NewClient(apiKey)
var wg sync.WaitGroup
errors := make(chan error, len(urls))
for i, url := range urls {
wg.Add(1)
go func(index int, u string) {
defer wg.Done()
filename := fmt.Sprintf("screenshot-%d.png", index)
err := client.Take(
screenshot.ScreenshotOptions{
URL: u,
FullPage: true,
},
filename,
)
if err != nil {
errors <- fmt.Errorf("%s: %w", u, err)
}
}(i, url)
}
wg.Wait()
close(errors)
// Check for errors
for err := range errors {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
}
return nil
}
func main() {
urls := []string{
"https://example.com",
"https://example.com/about",
"https://example.com/pricing",
}
if err := captureScreenshots(urls, os.Getenv("PAGEBOLT_API_KEY")); err != nil {
os.Exit(1)
}
fmt.Println("All screenshots complete")
}
Real Use Case: Web Monitoring Service
Monitor website changes:
package main
import (
"crypto/md5"
"fmt"
"io"
"os"
"time"
"yourmodule/screenshot"
)
// MonitorSite checks if a website has changed
func monitorSite(url string, apiKey string) error {
client := screenshot.NewClient(apiKey)
baselineFile := "baseline.png"
currentFile := "current.png"
// First run: create baseline
if _, err := os.Stat(baselineFile); os.IsNotExist(err) {
fmt.Println("Creating baseline...")
return client.Take(
screenshot.ScreenshotOptions{URL: url, FullPage: true},
baselineFile,
)
}
// Subsequent runs: compare
err := client.Take(
screenshot.ScreenshotOptions{URL: url, FullPage: true},
currentFile,
)
if err != nil {
return err
}
baseline, _ := os.ReadFile(baselineFile)
current, _ := os.ReadFile(currentFile)
baselineHash := fmt.Sprintf("%x", md5.Sum(baseline))
currentHash := fmt.Sprintf("%x", md5.Sum(current))
if baselineHash != currentHash {
fmt.Printf("CHANGED: %s has been modified\n", url)
fmt.Printf("Baseline: %s\n", baselineHash)
fmt.Printf("Current: %s\n", currentHash)
return nil
}
fmt.Println("No changes detected")
return nil
}
func main() {
// Run every hour
ticker := time.NewTicker(time.Hour)
defer ticker.Stop()
for {
if err := monitorSite("https://example.com", os.Getenv("PAGEBOLT_API_KEY")); err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
}
<-ticker.C
}
}
Testing: Mock the API
package screenshot
import (
"bytes"
"io"
"net/http"
"testing"
)
// MockTransport mocks HTTP responses
type MockTransport struct {
response []byte
status int
}
func (m *MockTransport) RoundTrip(req *http.Request) (*http.Response, error) {
return &http.Response{
StatusCode: m.status,
Body: io.NopCloser(bytes.NewReader(m.response)),
Header: make(http.Header),
}, nil
}
func TestTakeScreenshot(t *testing.T) {
mockData := []byte{137, 80, 78, 71} // PNG magic bytes
client := &Client{
apiKey: "test-key",
client: &http.Client{
Transport: &MockTransport{
response: mockData,
status: http.StatusOK,
},
},
}
data, err := client.TakeBytes(ScreenshotOptions{
URL: "https://example.com",
})
if err != nil {
t.Fatalf("TakeBytes failed: %v", err)
}
if !bytes.Equal(data, mockData) {
t.Errorf("Got %v, want %v", data, mockData)
}
}
Common Mistakes
| ❌ Wrong | ✅ Right |
|---|---|
json.Unmarshal(body, &var) |
io.ReadAll(resp.Body) |
ioutil.ReadAll() (deprecated) |
io.ReadAll() |
Assuming Content-Type: application/json
|
Binary response = PNG bytes |
err == nil without checking status |
resp.StatusCode == http.StatusOK |
| Closing body in defer after error | Defer before checking status |
Pricing
| Plan | Requests/Month | Cost | Best For |
|---|---|---|---|
| Free | 100 | $0 | Learning, low-volume projects |
| Starter | 5,000 | $29 | Small teams, moderate use |
| Growth | 25,000 | $79 | Production apps, frequent calls |
| Scale | 100,000 | $199 | High-volume automation |
Summary
- ✅ Idiomatic Go with
http.NewRequestand proper headers - ✅ Binary response handling with
io.ReadAll()(not JSON) - ✅ File writing with
os.WriteFile() - ✅ Concurrent requests with goroutines
- ✅ Structured client for reuse
- ✅ Easy testing with mock HTTP transport
- ✅ No external dependencies
Get started free: pagebolt.dev — 100 requests/month, no credit card required.
Top comments (0)