Skip to content

PHP examples

Snippets target PHP 8.1+. cURL works out of the box; for Guzzle:

Terminal window
composer require guzzlehttp/guzzle
<?php
const PHOTOPICK_API = 'https://api.photopick.cz/api/v1';
$token = getenv('PHOTOPICK_API_KEY'); // pp_live_xxxxxxxxxxxxxxxx
<?php
function pp_request(string $path, string $method = 'GET', ?array $body = null): array {
$ch = curl_init(PHOTOPICK_API . '/' . ltrim($path, '/'));
$headers = [
'Authorization: Bearer ' . getenv('PHOTOPICK_API_KEY'),
'Accept: application/json',
];
if ($body !== null) {
$headers[] = 'Content-Type: application/json';
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($body));
}
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => $method,
CURLOPT_HTTPHEADER => $headers,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => true,
]);
$raw = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($status >= 400) {
throw new RuntimeException("HTTP $status: $raw");
}
return json_decode($raw, true);
}
$me = pp_request('me');
print_r($me['scopes']);
print_r($me['rateLimit']);

Required scope: customer:read.

Generator yielding every photo across pages — constant memory:

<?php
use GuzzleHttp\Client;
function iterate_photos(Client $client, int $pageSize = 100): Generator {
$cursor = null;
do {
$query = ['limit' => $pageSize];
if ($cursor !== null) {
$query['cursor'] = $cursor;
}
$body = json_decode(
$client->get('photos', ['query' => $query])->getBody(),
true,
);
foreach ($body['data'] as $photo) {
yield $photo;
}
$cursor = $body['pagination']['hasMore']
? $body['pagination']['nextCursor']
: null;
} while ($cursor !== null);
}
foreach (iterate_photos($client) as $photo) {
echo $photo['id'] . ' ' . $photo['filename'] . PHP_EOL;
}

Required scope: photos:read.

Stream straight to disk so memory stays flat regardless of file size.

<?php
use GuzzleHttp\Client;
function download_photo(Client $client, int $photoId, string $destination): array {
$meta = json_decode($client->get("photos/$photoId")->getBody(), true);
// sink + stream avoids loading the whole file into memory
$client->get("photos/$photoId/download", [
'sink' => $destination,
'allow_redirects' => true,
]);
return $meta;
}
$meta = download_photo($client, 1234, __DIR__ . '/photo-1234.jpg');
echo "Saved {$meta['filename']} ({$meta['bytes']} bytes)" . PHP_EOL;

Required scope: photos:read.

<?php
use GuzzleHttp\Client;
// Create
$created = json_decode($client->post('tags', [
'json' => ['name' => 'Selects', 'color' => '#e94b60'],
])->getBody(), true);
// Attach (full replace — pass the complete tag list)
$client->put("photos/1234/tags", [
'json' => ['tagIds' => [$created['id']]],
]);
// Rename
$client->patch("tags/{$created['id']}", [
'json' => ['name' => 'Final selects'],
]);
// Delete
$client->delete("tags/{$created['id']}");

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

Guzzle ships with a middleware for retry; the pattern below makes the per-call intent explicit.

<?php
use GuzzleHttp\Client;
use GuzzleHttp\Exception\RequestException;
use Psr\Http\Message\ResponseInterface;
function with_retry(callable $call, int $maxAttempts = 5): ResponseInterface {
for ($attempt = 1; $attempt <= $maxAttempts; $attempt++) {
try {
$response = $call();
$status = $response->getStatusCode();
if ($status < 500 && $status !== 429) {
return $response;
}
} catch (RequestException $e) {
$response = $e->getResponse();
if ($response === null) {
throw $e; // network error — bail
}
$status = $response->getStatusCode();
if ($status < 500 && $status !== 429) {
throw $e;
}
}
$retryAfter = (int) ($response->getHeaderLine('Retry-After') ?: $attempt);
sleep(min($retryAfter, 2 ** $attempt));
}
throw new RuntimeException('retry budget exhausted');
}
$response = with_retry(fn() => $client->get('photos'));

See Rate limits and Errors for the full vocabulary.