TypeScript examples
Snippets target TypeScript 5.4+ on Node.js 22+ (native fetch, top-level await). They work unchanged in Deno and Bun. For the browser, drop the node: imports.
const API = 'https://api.photopick.cz/api/v1';const TOKEN = process.env.PHOTOPICK_API_KEY!; // pp_live_xxxxxxxxxxxxxxxxconst authHeaders = { Authorization: `Bearer ${TOKEN}` } as const;1. Verify your key (/me)
Section titled “1. Verify your key (/me)”A small typed wrapper that narrows Response to your payload and throws on non-2xx:
interface RateLimit { limit: number; remaining: number; reset: number }interface Me { apiKeyId: number customerId: number name: string scopes: string[] rateLimit: RateLimit}
async function pp<T>(path: string, init: RequestInit = {}): Promise<T> { const res = await fetch(`${API}${path}`, { ...init, headers: { ...authHeaders, ...(init.headers ?? {}) }, }); if (!res.ok) throw new Error(`HTTP ${res.status} on ${path}`); return res.status === 204 ? (undefined as T) : (res.json() as Promise<T>);}
const me = await pp<Me>('/me');console.log(me.scopes, me.rateLimit.remaining);Required scope: customer:read.
2. List photos (paginated)
Section titled “2. List photos (paginated)”Typed async generator — constant memory, every yielded value is Photo:
interface Photo { id: number filename: string bytes: number takenAt: string | null tagIds: number[]}
interface Page<T> { data: T[] pagination: { nextCursor: string | null; hasMore: boolean }}
async function* iteratePhotos(pageSize = 100): AsyncGenerator<Photo> { let cursor: string | undefined for (;;) { const url = new URL(`${API}/photos`) url.searchParams.set('limit', String(pageSize)) if (cursor) url.searchParams.set('cursor', cursor)
const page = await pp<Page<Photo>>(`/photos?${url.searchParams}`) for (const p of page.data) yield p if (!page.pagination.hasMore || !page.pagination.nextCursor) return cursor = page.pagination.nextCursor }}
for await (const photo of iteratePhotos()) { console.log(photo.id, photo.filename)}Required scope: photos:read.
3. Fetch one photo + download original
Section titled “3. Fetch one photo + download original”import { writeFile } from 'node:fs/promises'
async function downloadPhoto(photoId: number, outPath: string): Promise<Photo> { const meta = await pp<Photo>(`/photos/${photoId}`)
const res = await fetch(`${API}/photos/${photoId}/download`, { headers: authHeaders, redirect: 'follow', }) if (!res.ok) throw new Error(`HTTP ${res.status}`) await writeFile(outPath, new Uint8Array(await res.arrayBuffer()))
return meta}
const meta = await downloadPhoto(1234, './photo-1234.jpg')console.log(`Saved ${meta.filename} (${meta.bytes} bytes)`)Required scope: photos:read.
4. Tag CRUD
Section titled “4. Tag CRUD”interface Tag { id: number; name: string; color: string | null }
// Createconst created = await pp<Tag>('/tags', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name: 'Selects', color: '#e94b60' }),})
// Attach (full replace — pass the complete tag list)await pp<void>(`/photos/1234/tags`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ tagIds: [created.id] }),})
// Renameawait pp<Tag>(`/tags/${created.id}`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name: 'Final selects' }),})
// Deleteawait pp<void>(`/tags/${created.id}`, { method: 'DELETE' })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)”Generic retry that preserves the typed return value of the wrapped call:
async function withRetry<T>( call: () => Promise<Response>, { maxAttempts = 5 }: { maxAttempts?: number } = {},): Promise<Response> { for (let attempt = 1; attempt <= maxAttempts; attempt++) { const res = await call() if (res.ok || (res.status < 500 && res.status !== 429)) return res
const retryAfter = Number(res.headers.get('retry-after') ?? attempt) const backoff = Math.min(retryAfter, 2 ** attempt) await new Promise(r => setTimeout(r, backoff * 1000)) } throw new Error('retry budget exhausted')}
const res = await withRetry(() => fetch(`${API}/photos`, { headers: authHeaders }),)const body = (await res.json()) as Page<Photo>See Rate limits and Errors for the full vocabulary.