Use case · Bulk metadata

Bulk metadata for large ID lists

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.

The problem

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.

N detail calls

Calling GET /images/{id} once per asset turns 80 saved IDs into 80 rate units.

Stale local caches

Titles and tags change rarely—but previews and counts still need a fresh catalog pull on open.

Missing assets

Old boards may reference removed or invalid IDs; the UI must degrade gracefully, not fail entirely.

Selection explosion

Multi-select toolbars and “export all on page” flows routinely exceed 100 IDs.

Who it hurts

  • Moodboard / collection apps that reopen projects with long ID lists
  • Agency shared libraries syncing project manifests between tools
  • CMS illustrators that stored IDs on posts and need metadata at edit time
  • Export / POD pipelines validating a job queue before minting download tokens

Solution: 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.

Partial matches: 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
  • Build a set of returned IDs and compute missing = requestedIds − foundIds for UI warnings
  • Do not abort the whole board render when a few historical IDs 404-equivalent-omit
  • Empty array, >100 items, or non-unique IDs → validation error (HTTP 422)
  • Single unknown ID on GET /images/{id} is HTTP 404 image_not_found; batch omits instead

Chunking strategy for more than 100 IDs

When 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).

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.

Step-by-step implementation

  1. Collect unique IDs from the project manifest, selection set, or local database.
  2. If length ≤ 100, call POST /images/batch once with the full list.
  3. If length > 100, slice into chunks of 100 and call batch for each chunk.
  4. Merge images arrays; sum or track requested/found per chunk.
  5. Diff missing IDs for soft UI errors (placeholder cards, “asset unavailable”).
  6. Optional: for a single deep-link, prefer GET /images/{id} instead of batch.

Why batch beats N× GET

When to use GET one

Deep-link open, inspector refresh, single cache miss.

When to use batch

Multi-select, board open, cart/export list, offline rehydrate.

Combine with search

Discover via GET /images, store IDs, batch only when the UI needs full cards.

Then download

Mint original tokens serially—one concurrent transfer per account.

Code examples

Resolve two IDs in one request, then a chunked helper for larger lists. Always load the key from the server environment.

Single batch (≤ 100 IDs)

cURL
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"]}'

Chunked rehydrate (> 100 IDs)

Node
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 };
}

Rate-limit notes

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.

  • 1–100 IDs = 1 rate unit
  • 350 IDs ≈ 4 rate units if chunked as 100/100/100/50
  • 350× GET /images/{id} would be 350 rate units—unsustainable under the plan limit
  • Binary downloads use a separate concurrency lease; batch itself does not transfer originals

Next steps

Asset search

Discover IDs first with GET /images, then batch on demand.