N detail calls
Calling GET /images/{id} once per asset turns 80 saved IDs into 80 rate units.
Use case · Bulk metadata
Boards, export queues, and offline caches often hold hundreds of image IDs without full catalog records.
POST https://epochalstock.com/api/v1/images/batch resolves 1–100 unique IDs in a
single authenticated request—so you rehydrate UI state without burning the shared 30 req/min budget on
one-by-one lookups.
After discovery, most products stop storing full payloads and keep only stable image IDs. Later they must “rehydrate” titles, tags, dimensions, and safe preview URLs for selection panels, print queues, or team boards.
Calling GET /images/{id} once per asset turns 80 saved IDs into 80 rate units.
Titles and tags change rarely—but previews and counts still need a fresh catalog pull on open.
Old boards may reference removed or invalid IDs; the UI must degrade gracefully, not fail entirely.
Multi-select toolbars and “export all on page” flows routinely exceed 100 IDs.
POST /images/batch
Send a JSON body with an ids array. Each successful batch call counts as
one authenticated API request toward the account limit—not one request per ID.
| Item | Value |
|---|---|
| Method / path | POST /images/batch |
| Full URL | https://epochalstock.com/api/v1/images/batch |
| Auth | Authorization: Bearer … + Accept: application/json |
| Content-Type | application/json |
| Body | {"ids":["es_001_example","es_002_example"]} |
ids |
1–100 unique strings (dedupe client-side before calling) |
| Rate impact | One request per batch (not per ID) |
| Missing IDs | Omitted from results; compare found vs requested |
Field-level metadata matches single-image detail (title, tags, dimensions, thumbnail/preview URLs). Original filesystem paths are never returned—use POST /downloads for binaries. Full reference: Image detail & batch.
requested vs found
Batch is intentionally non-atomic for missing IDs. Unknown or retired IDs are simply left out of
images. The response includes counts so you can detect incomplete sets without treating the
whole call as a hard failure.
| Field | Type | Description |
|---|---|---|
images |
object[] | Found records (same shape as GET /images/{id}) |
requested |
integer | Number of IDs accepted after validation |
found |
integer | Number of records returned. If found < requested, some IDs were missing |
missing = requestedIds − foundIds for UI warningsGET /images/{id} is HTTP 404 image_not_found; batch omits insteadWhen a project holds more than 100 unique IDs, split into sequential chunks of at most 100. Each chunk is one rate unit. At 30 req/min you can theoretically rehydrate up to 3,000 IDs per minute if every call is a full batch of 100 (leave headroom for search and downloads).
Large list → chunked batch calls
Practice: dedupe and sort IDs for stable caching; run chunks serially (or with low
concurrency) so you do not stampede into 429; honor Retry-After when limited.
POST /images/batch once with the full list.images arrays; sum or track requested/found per chunk.Request budget comparison (example: 80 IDs)
Deep-link open, inspector refresh, single cache miss.
Multi-select, board open, cart/export list, offline rehydrate.
Discover via GET /images, store IDs, batch only when the UI needs full cards.
Mint original tokens serially—one concurrent transfer per account.
Resolve two IDs in one request, then a chunked helper for larger lists. Always load the key from the server environment.
curl -sS -X POST "https://epochalstock.com/api/v1/images/batch" \
-H "Authorization: Bearer $EPOCHAL_API_KEY" \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-d '{"ids":["es_001_example","es_002_example"]}'
const base = process.env.EPOCHAL_API_BASE ?? "https://epochalstock.com/api/v1";
const ids = ["es_001_example", "es_002_example"];
const response = await fetch(`${base}/images/batch`, {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.EPOCHAL_API_KEY}`,
"Content-Type": "application/json",
Accept: "application/json",
},
body: JSON.stringify({ ids }),
});
if (!response.ok) throw new Error(await response.text());
const { data } = await response.json();
console.log(`found ${data.found} of ${data.requested}`);
const foundIds = new Set(data.images.map((img) => img.id));
const missing = ids.filter((id) => !foundIds.has(id));
if (missing.length) console.warn("missing", missing);
$base = rtrim(getenv('EPOCHAL_API_BASE') ?: 'https://epochalstock.com/api/v1', '/');
$ids = ['es_001_example', 'es_002_example'];
$ch = curl_init($base . '/images/batch');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . getenv('EPOCHAL_API_KEY'),
'Content-Type: application/json',
'Accept: application/json',
],
CURLOPT_POSTFIELDS => json_encode(['ids' => $ids], JSON_THROW_ON_ERROR),
CURLOPT_TIMEOUT => 20,
]);
$response = curl_exec($ch);
$status = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($status !== 200) {
throw new RuntimeException((string) $response);
}
$data = json_decode($response, true, 512, JSON_THROW_ON_ERROR)['data'];
// $data['images'], $data['requested'], $data['found']
$foundIds = array_column($data['images'], 'id');
$missing = array_values(array_diff($ids, $foundIds));
import os
import requests
base = os.environ.get("EPOCHAL_API_BASE", "https://epochalstock.com/api/v1")
ids = ["es_001_example", "es_002_example"]
r = requests.post(
f"{base}/images/batch",
json={"ids": ids},
headers={
"Authorization": f"Bearer {os.environ['EPOCHAL_API_KEY']}",
"Accept": "application/json",
},
timeout=20,
)
r.raise_for_status()
data = r.json()["data"]
print(f"found {data['found']} of {data['requested']}")
found_ids = {img["id"] for img in data["images"]}
missing = [i for i in ids if i not in found_ids]
const base = process.env.EPOCHAL_API_BASE ?? "https://epochalstock.com/api/v1";
async function batchOnce(ids) {
const res = await fetch(`${base}/images/batch`, {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.EPOCHAL_API_KEY}`,
"Content-Type": "application/json",
Accept: "application/json",
},
body: JSON.stringify({ ids }),
});
if (res.status === 429) {
const retryAfter = Number(res.headers.get("Retry-After") || "2");
await new Promise((r) => setTimeout(r, retryAfter * 1000));
return batchOnce(ids);
}
if (!res.ok) throw new Error(await res.text());
return (await res.json()).data;
}
/** @param {string[]} allIds */
async function rehydrateAll(allIds) {
const unique = [...new Set(allIds)];
const images = [];
let requested = 0;
let found = 0;
for (let i = 0; i < unique.length; i += 100) {
const chunk = unique.slice(i, i + 100);
const data = await batchOnce(chunk);
images.push(...data.images);
requested += data.requested;
found += data.found;
}
const foundIds = new Set(images.map((img) => img.id));
const missing = unique.filter((id) => !foundIds.has(id));
return { images, requested, found, missing };
}
function batch_once(string $base, array $ids): array
{
$ch = curl_init(rtrim($base, '/') . '/images/batch');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . getenv('EPOCHAL_API_KEY'),
'Content-Type: application/json',
'Accept: application/json',
],
CURLOPT_POSTFIELDS => json_encode(['ids' => array_values($ids)], JSON_THROW_ON_ERROR),
CURLOPT_TIMEOUT => 20,
CURLOPT_HEADER => true,
]);
$raw = curl_exec($ch);
$status = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
$headerSize = (int) curl_getinfo($ch, CURLINFO_HEADER_SIZE);
curl_close($ch);
$headers = substr((string) $raw, 0, $headerSize);
$body = substr((string) $raw, $headerSize);
if ($status === 429) {
$retry = 2;
if (preg_match('/^Retry-After:\s*(\d+)/mi', $headers, $m)) {
$retry = max(1, (int) $m[1]);
}
sleep($retry);
return batch_once($base, $ids);
}
if ($status !== 200) {
throw new RuntimeException($body);
}
return json_decode($body, true, 512, JSON_THROW_ON_ERROR)['data'];
}
function rehydrate_all(array $allIds): array
{
$base = getenv('EPOCHAL_API_BASE') ?: 'https://epochalstock.com/api/v1';
$unique = array_values(array_unique($allIds));
$images = [];
$requested = 0;
$found = 0;
foreach (array_chunk($unique, 100) as $chunk) {
$data = batch_once($base, $chunk);
$images = array_merge($images, $data['images']);
$requested += (int) $data['requested'];
$found += (int) $data['found'];
}
$foundIds = array_column($images, 'id');
$missing = array_values(array_diff($unique, $foundIds));
return compact('images', 'requested', 'found', 'missing');
}
import os
import time
import requests
base = os.environ.get("EPOCHAL_API_BASE", "https://epochalstock.com/api/v1")
headers = {
"Authorization": f"Bearer {os.environ['EPOCHAL_API_KEY']}",
"Accept": "application/json",
}
def batch_once(ids):
while True:
r = requests.post(
f"{base}/images/batch",
json={"ids": ids},
headers=headers,
timeout=20,
)
if r.status_code == 429:
time.sleep(int(r.headers.get("Retry-After", "2")))
continue
r.raise_for_status()
return r.json()["data"]
def rehydrate_all(all_ids):
unique = list(dict.fromkeys(all_ids)) # preserve order, dedupe
images = []
requested = found = 0
for i in range(0, len(unique), 100):
chunk = unique[i : i + 100]
data = batch_once(chunk)
images.extend(data["images"])
requested += data["requested"]
found += data["found"]
found_ids = {img["id"] for img in images}
missing = [i for i in unique if i not in found_ids]
return {
"images": images,
"requested": requested,
"found": found,
"missing": missing,
}
# Chunk manually when scripting large lists (example: first 100)
curl -sS -X POST "https://epochalstock.com/api/v1/images/batch" \
-H "Authorization: Bearer $EPOCHAL_API_KEY" \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-d @chunk-001.json
# chunk-001.json → {"ids":["es_…", … up to 100]}
Batch is the main tool for staying inside the 30 requests/minute per account ceiling when hydrating large sets. Creating additional API keys does not increase the shared budget.
Aggressive parallel chunking can still trip rate_limit_exceeded (HTTP 429). Prefer serial
chunks or a small worker pool; always respect Retry-After and keep
X-Request-ID / meta.request_id in logs.
GET /images/{id} would be 350 rate units—unsustainable under the plan limitDetails: Rate limits & errors.
Field reference for single GET and POST batch, plus sample responses.
30 req/min headers, 429 handling, and the full error code table.
Discover IDs first with GET /images, then batch on demand.
Two-step tokens after metadata is confirmed present.