Skip to content

JavaScript examples

All snippets assume Node.js 22+ or any modern browser. fetch is built in. For axios, install with pnpm add axios.

// Recommended setup
const API = 'https://api.photopick.cz/api/v1';
const TOKEN = process.env.PHOTOPICK_API_KEY; // pp_live_xxxxxxxxxxxxxxxx
const authHeaders = { Authorization: `Bearer ${TOKEN}` };
const res = await fetch(`${API}/me`, { headers: authHeaders });
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const me = await res.json();
console.log(me.scopes, me.rateLimit);

Required scope: customer:read.

Walk the full collection with cursor pagination:

async function* iteratePhotos({ pageSize = 100 } = {}) {
let cursor = undefined;
do {
const url = new URL(`${API}/photos`);
url.searchParams.set('limit', String(pageSize));
if (cursor) url.searchParams.set('cursor', cursor);
const res = await fetch(url, { headers: authHeaders });
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const body = await res.json();
for (const photo of body.data) yield photo;
cursor = body.pagination.hasMore ? body.pagination.nextCursor : null;
} while (cursor);
}
// Usage — process every photo without holding the full list in memory.
for await (const photo of iteratePhotos()) {
console.log(photo.id, photo.filename);
}

Required scope: photos:read.

async function downloadPhoto(photoId, outPath) {
const meta = await fetch(`${API}/photos/${photoId}`, { headers: authHeaders })
.then((r) => r.json());
const res = await fetch(`${API}/photos/${photoId}/download`, {
headers: authHeaders,
redirect: 'follow',
});
if (!res.ok) throw new Error(`HTTP ${res.status}`);
// Node.js stream-to-file
const { writeFile } = await import('node:fs/promises');
await writeFile(outPath, Buffer.from(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.

// Create
const created = await fetch(`${API}/tags`, {
method: 'POST',
headers: { ...authHeaders, 'Content-Type': 'application/json' },
body: JSON.stringify({ name: 'Selects', color: '#e94b60' }),
}).then((r) => r.json());
// Attach (full replace — pass the complete tag list)
await fetch(`${API}/photos/1234/tags`, {
method: 'PUT',
headers: { ...authHeaders, 'Content-Type': 'application/json' },
body: JSON.stringify({ tagIds: [created.id] }),
});
// Rename
await fetch(`${API}/tags/${created.id}`, {
method: 'PATCH',
headers: { ...authHeaders, 'Content-Type': 'application/json' },
body: JSON.stringify({ name: 'Final selects' }),
});
// Delete
const del = await fetch(`${API}/tags/${created.id}`, {
method: 'DELETE',
headers: authHeaders,
});
console.log(del.status); // 204

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

Wrap any request that might hit 429 or transient 5xx:

async function withRetry(call, { maxAttempts = 5 } = {}) {
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 }),
);

See Rate limits and Errors for the full vocabulary.