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)}1. Verify your key (/me)
Section titled “1. Verify your key (/me)”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)}
// Usageme, err := photopick.New().Me(context.Background())if err != nil { log.Fatal(err) }fmt.Println(me.Scopes, me.RateLimit.Remaining)Required scope: customer:read.
2. List photos (paginated)
Section titled “2. List photos (paginated)”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}
// Usagephotos, 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.
3. Fetch one photo + download original
Section titled “3. Fetch one photo + download original”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.
4. Tag CRUD
Section titled “4. Tag CRUD”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)}
// Createcreated := 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)
// Renameerr = c.postJSON(ctx, http.MethodPatch, fmt.Sprintf("/tags/%d", created.ID), Tag{Name: "Final selects"}, &created)
// Deleteerr = 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).
Retry pattern (rate limits + 5xx)
Section titled “Retry pattern (rate limits + 5xx)”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.