Skip to content

Go examples

Snippets target Go 1.22+. They use only the standard library (net/http, encoding/json, io) — no third-party dependencies required.

package photopick
import (
"context"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"net/url"
"os"
"time"
)
const API = "https://api.photopick.cz/api/v1"
type Client struct {
HTTP *http.Client
BaseURL string
Token string
}
func New() *Client {
return &Client{
HTTP: &http.Client{Timeout: 30 * time.Second},
BaseURL: API,
Token: os.Getenv("PHOTOPICK_API_KEY"), // pp_live_xxxxxxxxxxxxxxxx
}
}
func (c *Client) do(ctx context.Context, method, path string, body io.Reader) (*http.Response, error) {
req, err := http.NewRequestWithContext(ctx, method, c.BaseURL+path, body)
if err != nil {
return nil, err
}
req.Header.Set("Authorization", "Bearer "+c.Token)
req.Header.Set("Accept", "application/json")
if body != nil {
req.Header.Set("Content-Type", "application/json")
}
return c.HTTP.Do(req)
}
type RateLimit struct {
Limit int `json:"limit"`
Remaining int `json:"remaining"`
Reset int `json:"reset"`
}
type Me struct {
APIKeyID int `json:"apiKeyId"`
CustomerID int `json:"customerId"`
Name string `json:"name"`
Scopes []string `json:"scopes"`
RateLimit RateLimit `json:"rateLimit"`
}
func (c *Client) Me(ctx context.Context) (*Me, error) {
res, err := c.do(ctx, http.MethodGet, "/me", nil)
if err != nil {
return nil, err
}
defer res.Body.Close()
if res.StatusCode >= 400 {
return nil, fmt.Errorf("HTTP %d on /me", res.StatusCode)
}
var me Me
return &me, json.NewDecoder(res.Body).Decode(&me)
}
// Usage
me, err := photopick.New().Me(context.Background())
if err != nil { log.Fatal(err) }
fmt.Println(me.Scopes, me.RateLimit.Remaining)

Required scope: customer:read.

Channel-based iterator — flat memory, easy to cancel via context:

type Photo struct {
ID int `json:"id"`
Filename string `json:"filename"`
Bytes int64 `json:"bytes"`
TakenAt *string `json:"takenAt"`
TagIDs []int `json:"tagIds"`
}
type Page struct {
Data []Photo `json:"data"`
Pagination struct {
NextCursor *string `json:"nextCursor"`
HasMore bool `json:"hasMore"`
} `json:"pagination"`
}
func (c *Client) IteratePhotos(ctx context.Context, pageSize int) (<-chan Photo, <-chan error) {
out := make(chan Photo)
errs := make(chan error, 1)
go func() {
defer close(out)
defer close(errs)
cursor := ""
for {
q := url.Values{"limit": {fmt.Sprint(pageSize)}}
if cursor != "" {
q.Set("cursor", cursor)
}
res, err := c.do(ctx, http.MethodGet, "/photos?"+q.Encode(), nil)
if err != nil {
errs <- err
return
}
var page Page
if err := json.NewDecoder(res.Body).Decode(&page); err != nil {
res.Body.Close()
errs <- err
return
}
res.Body.Close()
for _, p := range page.Data {
select {
case <-ctx.Done():
errs <- ctx.Err()
return
case out <- p:
}
}
if !page.Pagination.HasMore || page.Pagination.NextCursor == nil {
return
}
cursor = *page.Pagination.NextCursor
}
}()
return out, errs
}
// Usage
photos, errs := client.IteratePhotos(ctx, 100)
for p := range photos {
fmt.Println(p.ID, p.Filename)
}
if err := <-errs; err != nil {
log.Fatal(err)
}

Required scope: photos:read.

io.Copy streams the body to disk without buffering the full file in RAM.

func (c *Client) DownloadPhoto(ctx context.Context, photoID int, dest string) (*Photo, error) {
// Metadata
res, err := c.do(ctx, http.MethodGet, fmt.Sprintf("/photos/%d", photoID), nil)
if err != nil {
return nil, err
}
var meta Photo
if err := json.NewDecoder(res.Body).Decode(&meta); err != nil {
res.Body.Close()
return nil, err
}
res.Body.Close()
// Binary — http.Client follows redirects to the signed URL automatically.
bin, err := c.do(ctx, http.MethodGet, fmt.Sprintf("/photos/%d/download", photoID), nil)
if err != nil {
return nil, err
}
defer bin.Body.Close()
if bin.StatusCode != 200 {
return nil, fmt.Errorf("HTTP %d", bin.StatusCode)
}
f, err := os.Create(dest)
if err != nil {
return nil, err
}
defer f.Close()
if _, err := io.Copy(f, bin.Body); err != nil {
return nil, err
}
return &meta, nil
}

Required scope: photos:read.

type Tag struct {
ID int `json:"id,omitempty"`
Name string `json:"name"`
Color string `json:"color,omitempty"`
}
func (c *Client) postJSON(ctx context.Context, method, path string, in, out any) error {
var body io.Reader
if in != nil {
buf, err := json.Marshal(in)
if err != nil {
return err
}
body = bytes.NewReader(buf)
}
res, err := c.do(ctx, method, path, body)
if err != nil {
return err
}
defer res.Body.Close()
if res.StatusCode >= 400 {
return fmt.Errorf("HTTP %d on %s", res.StatusCode, path)
}
if out == nil || res.StatusCode == http.StatusNoContent {
return nil
}
return json.NewDecoder(res.Body).Decode(out)
}
// Create
created := Tag{}
err := c.postJSON(ctx, http.MethodPost, "/tags",
Tag{Name: "Selects", Color: "#e94b60"}, &created)
// Attach (full replace — pass the complete tag list)
type tagIDs struct{ TagIDs []int `json:"tagIds"` }
err = c.postJSON(ctx, http.MethodPut, "/photos/1234/tags",
tagIDs{TagIDs: []int{created.ID}}, nil)
// Rename
err = c.postJSON(ctx, http.MethodPatch, fmt.Sprintf("/tags/%d", created.ID),
Tag{Name: "Final selects"}, &created)
// Delete
err = c.postJSON(ctx, http.MethodDelete, fmt.Sprintf("/tags/%d", created.ID),
nil, nil)

Required scopes: tags:write (create/rename/delete), photos:write (attach), tags:read (list).

func (c *Client) doWithRetry(ctx context.Context, method, path string, body io.Reader, maxAttempts int) (*http.Response, error) {
for attempt := 1; attempt <= maxAttempts; attempt++ {
res, err := c.do(ctx, method, path, body)
if err != nil {
return nil, err
}
if res.StatusCode < 500 && res.StatusCode != http.StatusTooManyRequests {
return res, nil
}
retryAfter := attempt
if h := res.Header.Get("Retry-After"); h != "" {
if n, err := strconv.Atoi(h); err == nil {
retryAfter = n
}
}
res.Body.Close()
backoff := time.Duration(min(retryAfter, 1<<attempt)) * time.Second
select {
case <-ctx.Done():
return nil, ctx.Err()
case <-time.After(backoff):
}
}
return nil, errors.New("retry budget exhausted")
}

See Rate limits and Errors for the full vocabulary.