Who builds it
POD platforms, merch agencies, packaging studios, calendar/photo-book apps, and internal brand portals that feed print vendors.
App blueprint · Print / POD
Print-on-demand stores, merch designers, and packaging tools need two things from stock: safe discovery (previews while the operator decides) and print-ready originals (full resolution when a SKU is approved). This blueprint wires Epochal Stock as the licensed source catalog—search and metadata on your backend, serial two-step downloads for files—while mockup compositing, print profiles, and garment overlays remain entirely in your application.
Operators browse the Epochal catalog through your admin or design UI, pin candidates to a product or campaign, approve one or more assets, then enqueue a print-export job. Your workers mint download tokens, pull originals one at a time (account concurrency is 1), store files in private object storage, and hand paths to your mockup and RIP/print pipeline.
POD platforms, merch agencies, packaging studios, calendar/photo-book apps, and internal brand portals that feed print vendors.
Search + tag filters, preview grid, product board, “approve for print,” job progress (“3 of 12 originals”), mockup gallery generated by your renderer.
Licensed catalog search, metadata, tags, stats, and full-resolution originals via documented download tokens—not mockup templates or print ICC conversion.
Single $50/month plan, full catalog, 30 req/min shared account limit, unlimited serial originals.
Architecture rule: browser / design client → your backend →
Epochal API. Never put EPOCHAL_API_KEY in storefront JavaScript, mobile apps, or
plugin packages. Binary download URLs are minted and redeemed on workers you control.
Split interactive catalog traffic from heavy export work. Search and previews stay on low-latency API routes with caching where safe; originals run through a single-flight export worker that respects the shared download lease.
Source catalog on Epochal · composite in your app
| Stage | Your system | Epochal API |
|---|---|---|
| Discover | GET /api/pod/search proxy |
GET /images |
| Tag browse | Cached chip list | GET /tags |
| Inspect | Detail drawer | GET /images/{id} |
| Board open | Rehydrate many pins | POST /images/batch |
| Approve | Write job rows in your DB | None |
| Export original | Serial worker + mutex | POST /downloads → token GET |
| Mockup / print | Your compositor, templates, color pipeline | None |
| Ops | Internal dashboard cache | GET /stats |
pod-export-prod; staging keys still share the same 30 req/min and download lease.image_id against jobs owned by the authenticated operator/org.GET /images with q, tags,
tag_mode, sort, limit, and optional cursor.
thumbnail_url / preview_url
and titles. Optional detail call for dimensions and description before pin.
image_id on a product, campaign, or print job
in your database (role: artwork, background, sleeve, etc.).
found vs requested).
409 download_already_active, backoff and retry.
Discovery uses the same documented catalog search as design tools. For print workflows, emphasize dimensions and extension after selection—not only aesthetics in the grid.
qTheme or subject (“mountain lake”, “botanical line art”). Max 200 characters.
tags + tag_mode
Filter chips from GET /tags. Use all for strict AND,
any for broader multi-select.
sortrelevance with text; newest when browsing fresh inventory.
Watermarked previews are for decision-making. They are not substitutes for print-ready originals—export when approved.
Tip: After multi-select approve, call
POST /images/batch
once (or chunked) so the export UI can show real width/height/extension before minting tokens.
See Bulk metadata.
Epochal does not render mockups. There is no documented endpoint for t-shirt draping, frame composites, “smart object” placement, or print-shop packaging. Your product owns templates, 2D/3D mockup engines, bleed settings, and vendor export. Epochal supplies licensed source pixels (and metadata) so those pipelines start from a clear commercial catalog.
Local path or private URL to the original you exported, template ID, placement rect, color profile, output DPI.
Customer-facing mockup JPEGs/WebPs, plus print packages (PDF/TIFF/PNG) for fulfillment.
Do not document fake Epochal routes like /mockups, /print, or
/composite. Keep those on your domain.
Use originals under the Epochal Stock commercial license; do not resell the raw catalog as a competing stock library.
Full originals use the two-step download flow. Accounts may run one concurrent original transfer. Serial downloads are otherwise unlimited—design for a single consumer (or a distributed mutex keyed by account), not a parallel fan-out.
Queue · lock · mint · GET · store · unlock · mockup
Database rows or a queue service with status
queued → exporting → exported → composited
/ failed.
Tokens last about five minutes and are single-use. Create the token only when the worker is ready to stream the binary.
Set generous client timeouts for the binary GET. A hung transfer may hold the concurrency lease.
Show “exporting 4 of 18” rather than spinning 18 parallel progress bars that will 409.
POST https://epochalstock.com/api/v1/downloads with Bearer key and
{"image_id":"…"} → 201 + download_url.
GET the URL immediately without the API key; write to private storage.
image_id.
Canonical export playbook: Safe original export pipeline.
409 download_already_active
If any process on the account is already transferring an original, further mint attempts return
HTTP 409 with download_already_active. Treat this as a scheduler signal,
not a permanent catalog failure.
| Situation | What your POD worker should do |
|---|---|
| Your export worker is mid-GET | Do not mint a second job. Wait until the current transfer finishes, then continue serially. |
| Staging and prod keys share the account | Coordinate schedules or accept backoff; keys share one lease and one 30 req/min budget. |
| Two deployments race | Catch 409, re-queue with exponential backoff + jitter, keep job status queued. |
| Hung connection | Ensure HTTP timeouts and connection cleanup so the lease can clear; then retry mint. |
| Rate limit during mint | HTTP 429 / rate_limit_exceeded—honor Retry-After. |
Prefer prevention: a local mutex or single-consumer queue avoids most 409s. Still implement remote 409 handling for multi-region workers and accidental double deploys.
Server/worker only. Default base URL: https://epochalstock.com/api/v1.
Never expose EPOCHAL_API_KEY to browsers or storefronts.
curl -sS "https://epochalstock.com/api/v1/images?q=botanical+pattern&tags=nature,floral&tag_mode=all&sort=relevance&limit=30" \
-H "Authorization: Bearer $EPOCHAL_API_KEY" \
-H "Accept: application/json"
const base = process.env.EPOCHAL_API_BASE ?? "https://epochalstock.com/api/v1";
const apiKey = process.env.EPOCHAL_API_KEY;
if (!apiKey) throw new Error("Set EPOCHAL_API_KEY on the POD backend");
/** Operator-facing search — session auth happens in your framework */
async function searchAssets({ q, tags, tagMode = "all", sort = "relevance", limit = 30, cursor }) {
const params = new URLSearchParams({ sort, limit: String(limit) });
if (q) params.set("q", String(q).slice(0, 200));
if (tags) {
const list = Array.isArray(tags) ? tags : String(tags).split(",");
params.set("tags", list.map((t) => t.trim()).filter(Boolean).slice(0, 20).join(","));
params.set("tag_mode", tagMode === "any" ? "any" : "all");
}
if (cursor) params.set("cursor", cursor);
const res = await fetch(`${base}/images?${params}`, {
headers: { Authorization: `Bearer ${apiKey}`, Accept: "application/json" },
});
if (res.status === 429) {
const wait = Number(res.headers.get("Retry-After") || "2");
await new Promise((r) => setTimeout(r, wait * 1000));
return searchAssets({ q, tags, tagMode, sort, limit, cursor });
}
if (!res.ok) throw new Error(await res.text());
const { data, meta } = await res.json();
// Return only preview fields to the operator UI
return {
images: data.images.map((img) => ({
id: img.id,
title: img.title,
width: img.width,
height: img.height,
extension: img.extension,
tags: img.tags,
thumbnail_url: img.thumbnail_url,
preview_url: img.preview_url,
})),
total: data.total,
nextCursor: data.next_cursor ?? null,
requestId: meta?.request_id,
};
}
<?php
declare(strict_types=1);
$base = rtrim(getenv('EPOCHAL_API_BASE') ?: 'https://epochalstock.com/api/v1', '/');
$apiKey = getenv('EPOCHAL_API_KEY');
if (!$apiKey) {
throw new RuntimeException('Set EPOCHAL_API_KEY on the POD backend');
}
/**
* @param array{q?:string,tags?:list<string>|string,tag_mode?:string,sort?:string,limit?:int,cursor?:string} $opts
* @return array<string,mixed>
*/
function search_assets(string $base, string $apiKey, array $opts): array
{
$params = [
'sort' => $opts['sort'] ?? 'relevance',
'limit' => $opts['limit'] ?? 30,
];
if (!empty($opts['q'])) {
$params['q'] = substr((string) $opts['q'], 0, 200);
}
if (!empty($opts['tags'])) {
$tags = is_array($opts['tags']) ? $opts['tags'] : explode(',', (string) $opts['tags']);
$tags = array_slice(array_values(array_filter(array_map('trim', $tags))), 0, 20);
if ($tags) {
$params['tags'] = implode(',', $tags);
$params['tag_mode'] = (($opts['tag_mode'] ?? 'all') === 'any') ? 'any' : 'all';
}
}
if (!empty($opts['cursor'])) {
$params['cursor'] = $opts['cursor'];
}
$ch = curl_init($base . '/images?' . http_build_query($params));
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . $apiKey,
'Accept: application/json',
],
CURLOPT_TIMEOUT => 20,
]);
$raw = curl_exec($ch);
$status = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($status === 429) {
sleep(2);
return search_assets($base, $apiKey, $opts);
}
if ($status !== 200) {
throw new RuntimeException((string) $raw);
}
return json_decode((string) $raw, true, 512, JSON_THROW_ON_ERROR)['data'];
}
import os
import time
import requests
base = os.environ.get("EPOCHAL_API_BASE", "https://epochalstock.com/api/v1").rstrip("/")
api_key = os.environ["EPOCHAL_API_KEY"]
def search_assets(*, q=None, tags=None, tag_mode="all", sort="relevance", limit=30, cursor=None):
params = {"sort": sort, "limit": limit}
if q:
params["q"] = q[:200]
if tags:
if isinstance(tags, list):
tags = ",".join(tags[:20])
params["tags"] = tags
params["tag_mode"] = "any" if tag_mode == "any" else "all"
if cursor:
params["cursor"] = cursor
r = requests.get(
f"{base}/images",
params=params,
headers={"Authorization": f"Bearer {api_key}", "Accept": "application/json"},
timeout=20,
)
if r.status_code == 429:
time.sleep(int(r.headers.get("Retry-After", "2")))
return search_assets(q=q, tags=tags, tag_mode=tag_mode, sort=sort, limit=limit, cursor=cursor)
r.raise_for_status()
return r.json()["data"]
package main
import (
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"os"
"strings"
"time"
)
func searchAssets(base, apiKey, q string, tags []string, limit int, cursor string) (map[string]any, error) {
v := url.Values{}
v.Set("sort", "relevance")
v.Set("limit", fmt.Sprintf("%d", limit))
if q != "" {
if len(q) > 200 {
q = q[:200]
}
v.Set("q", q)
}
if len(tags) > 0 {
if len(tags) > 20 {
tags = tags[:20]
}
v.Set("tags", strings.Join(tags, ","))
v.Set("tag_mode", "all")
}
if cursor != "" {
v.Set("cursor", cursor)
}
req, _ := http.NewRequest(http.MethodGet, strings.TrimRight(base, "/")+"/images?"+v.Encode(), nil)
req.Header.Set("Authorization", "Bearer "+apiKey)
req.Header.Set("Accept", "application/json")
res, err := (&http.Client{Timeout: 20 * time.Second}).Do(req)
if err != nil {
return nil, err
}
defer res.Body.Close()
b, _ := io.ReadAll(res.Body)
if res.StatusCode != 200 {
return nil, fmt.Errorf("search %d: %s", res.StatusCode, b)
}
var payload map[string]any
if err := json.Unmarshal(b, &payload); err != nil {
return nil, err
}
return payload["data"].(map[string]any), nil
}
func main() {
base := os.Getenv("EPOCHAL_API_BASE")
if base == "" {
base = "https://epochalstock.com/api/v1"
}
_ = base
}
curl -sS -X POST "https://epochalstock.com/api/v1/images/batch" \
-H "Authorization: Bearer $EPOCHAL_API_KEY" \
-H "Content-Type: application/json" \
-d '{"ids":["es_001_example","es_002_example"]}'
const base = process.env.EPOCHAL_API_BASE ?? "https://epochalstock.com/api/v1";
async function preflight(ids) {
const unique = [...new Set(ids)];
const images = [];
for (let i = 0; i < unique.length; i += 100) {
const chunk = unique.slice(i, i + 100);
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: chunk }),
});
if (!res.ok) throw new Error(await res.text());
const { data } = await res.json();
images.push(...data.images);
// data.found / data.requested — drop missing IDs from the export queue
}
return images;
}
<?php
function preflight(string $base, string $apiKey, array $ids): array
{
$unique = array_values(array_unique($ids));
$images = [];
foreach (array_chunk($unique, 100) as $chunk) {
$ch = curl_init(rtrim($base, '/') . '/images/batch');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . $apiKey,
'Content-Type: application/json',
'Accept: application/json',
],
CURLOPT_POSTFIELDS => json_encode(['ids' => $chunk], JSON_THROW_ON_ERROR),
CURLOPT_TIMEOUT => 20,
]);
$raw = curl_exec($ch);
$status = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($status !== 200) {
throw new RuntimeException((string) $raw);
}
$data = json_decode((string) $raw, true, 512, JSON_THROW_ON_ERROR)['data'];
foreach ($data['images'] as $img) {
$images[] = $img;
}
}
return $images;
}
import os
import requests
base = os.environ.get("EPOCHAL_API_BASE", "https://epochalstock.com/api/v1").rstrip("/")
def preflight(ids: list[str]) -> list[dict]:
unique = list(dict.fromkeys(ids))
images: list[dict] = []
for i in range(0, len(unique), 100):
chunk = unique[i : i + 100]
r = requests.post(
f"{base}/images/batch",
json={"ids": chunk},
headers={
"Authorization": f"Bearer {os.environ['EPOCHAL_API_KEY']}",
"Accept": "application/json",
},
timeout=20,
)
r.raise_for_status()
data = r.json()["data"]
images.extend(data["images"])
# inspect data["found"], data["requested"]
return images
Single-flight worker loop with 409/429 backoff. Composite step is a stub—your code, not Epochal.
import { writeFile } from "node:fs/promises";
import { join } from "node:path";
const base = process.env.EPOCHAL_API_BASE ?? "https://epochalstock.com/api/v1";
const apiKey = process.env.EPOCHAL_API_KEY;
async function sleep(ms) {
return new Promise((r) => setTimeout(r, ms));
}
async function mintAndDownload(imageId, destPath, attempt = 1) {
const mint = await fetch(`${base}/downloads`, {
method: "POST",
headers: {
Authorization: `Bearer ${apiKey}`,
"Content-Type": "application/json",
Accept: "application/json",
},
body: JSON.stringify({ image_id: imageId }),
});
if (mint.status === 409) {
// download_already_active
if (attempt >= 8) throw new Error("lease busy too long");
await sleep(Math.min(30_000, 500 * 2 ** (attempt - 1)) + Math.random() * 250);
return mintAndDownload(imageId, destPath, attempt + 1);
}
if (mint.status === 429) {
await sleep(Number(mint.headers.get("Retry-After") || "5") * 1000);
return mintAndDownload(imageId, destPath, attempt + 1);
}
if (mint.status === 404) {
const body = await mint.json().catch(() => ({}));
const code = body?.error?.code;
// image_not_found | original_unavailable — fail job permanently
throw new Error(`permanent ${code || mint.status}`);
}
if (!mint.ok) throw new Error(`mint ${mint.status}: ${await mint.text()}`);
const { data } = await mint.json();
const fileRes = await fetch(data.download_url); // no API key
if (!fileRes.ok) throw new Error(`download ${fileRes.status}`);
await writeFile(destPath, Buffer.from(await fileRes.arrayBuffer()));
}
/** YOUR app — not an Epochal endpoint */
async function renderMockup({ originalPath, templateId, productId }) {
// e.g. sharp / ImageMagick / 3D mockup service
return { mockupPath: `/var/pod/mockups/${productId}.jpg`, templateId, originalPath };
}
/**
* jobs: [{ id, imageId, productId, templateId }]
* Process originals serially; mockups may run after each file is local.
*/
async function runExportQueue(jobs, storageDir) {
for (const job of jobs) {
const dest = join(storageDir, `${job.imageId}.bin`);
await mintAndDownload(job.imageId, dest);
await renderMockup({
originalPath: dest,
templateId: job.templateId,
productId: job.productId,
});
// mark job complete in your DB
}
}
<?php
declare(strict_types=1);
$base = rtrim(getenv('EPOCHAL_API_BASE') ?: 'https://epochalstock.com/api/v1', '/');
$apiKey = getenv('EPOCHAL_API_KEY');
if (!$apiKey) {
throw new RuntimeException('Set EPOCHAL_API_KEY');
}
function mint_and_download(string $base, string $apiKey, string $imageId, string $destPath, int $attempt = 1): void
{
$ch = curl_init(rtrim($base, '/') . '/downloads');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . $apiKey,
'Content-Type: application/json',
'Accept: application/json',
],
CURLOPT_POSTFIELDS => json_encode(['image_id' => $imageId], JSON_THROW_ON_ERROR),
CURLOPT_TIMEOUT => 20,
]);
$raw = curl_exec($ch);
$status = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($status === 409) {
if ($attempt >= 8) {
throw new RuntimeException('lease busy too long');
}
usleep((int) ((500 * (2 ** ($attempt - 1))) + random_int(0, 250)) * 1000);
mint_and_download($base, $apiKey, $imageId, $destPath, $attempt + 1);
return;
}
if ($status === 429) {
sleep(5);
mint_and_download($base, $apiKey, $imageId, $destPath, $attempt + 1);
return;
}
if ($status === 404) {
throw new RuntimeException('permanent failure: ' . (string) $raw);
}
if ($status !== 201) {
throw new RuntimeException('mint failed: ' . (string) $raw);
}
$url = json_decode((string) $raw, true, 512, JSON_THROW_ON_ERROR)['data']['download_url'];
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => true,
CURLOPT_TIMEOUT => 120,
]);
$bytes = curl_exec($ch);
$dl = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($dl !== 200) {
throw new RuntimeException('download failed: ' . $dl);
}
file_put_contents($destPath, $bytes);
}
/** YOUR compositor — not Epochal */
function render_mockup(string $originalPath, string $templateId, string $productId): string
{
// Invoke ImageMagick, a PDF engine, etc.
return '/var/pod/mockups/' . $productId . '.jpg';
}
/**
* @param list<array{image_id:string,product_id:string,template_id:string}> $jobs
*/
function run_export_queue(string $base, string $apiKey, array $jobs, string $storageDir): void
{
foreach ($jobs as $job) {
$dest = rtrim($storageDir, '/\\') . DIRECTORY_SEPARATOR . $job['image_id'] . '.bin';
mint_and_download($base, $apiKey, $job['image_id'], $dest);
render_mockup($dest, $job['template_id'], $job['product_id']);
// UPDATE pod_jobs SET status = 'composited' …
}
}
import os
import random
import time
from pathlib import Path
import requests
base = os.environ.get("EPOCHAL_API_BASE", "https://epochalstock.com/api/v1").rstrip("/")
api_key = os.environ["EPOCHAL_API_KEY"]
def mint_and_download(image_id: str, dest: Path, attempt: int = 1) -> None:
mint = requests.post(
f"{base}/downloads",
headers={
"Authorization": f"Bearer {api_key}",
"Content-Type": "application/json",
"Accept": "application/json",
},
json={"image_id": image_id},
timeout=20,
)
if mint.status_code == 409:
if attempt >= 8:
raise RuntimeError("lease busy too long")
time.sleep(min(30, 0.5 * (2 ** (attempt - 1))) + random.random() * 0.25)
return mint_and_download(image_id, dest, attempt + 1)
if mint.status_code == 429:
time.sleep(int(mint.headers.get("Retry-After", "5")))
return mint_and_download(image_id, dest, attempt + 1)
if mint.status_code == 404:
raise RuntimeError(f"permanent: {mint.text}")
mint.raise_for_status()
url = mint.json()["data"]["download_url"]
with requests.get(url, stream=True, timeout=120) as r:
r.raise_for_status()
with open(dest, "wb") as f:
for chunk in r.iter_content(256 * 1024):
if chunk:
f.write(chunk)
def render_mockup(original: Path, template_id: str, product_id: str) -> Path:
# YOUR mockup pipeline (Pillow, skia, external service, …)
out = Path("/var/pod/mockups") / f"{product_id}.jpg"
return out
def run_export_queue(jobs: list[dict], storage_dir: Path) -> None:
storage_dir.mkdir(parents=True, exist_ok=True)
for job in jobs:
dest = storage_dir / f"{job['image_id']}.bin"
mint_and_download(job["image_id"], dest)
render_mockup(dest, job["template_id"], job["product_id"])
package main
import (
"bytes"
"encoding/json"
"fmt"
"io"
"math"
"math/rand"
"net/http"
"os"
"path/filepath"
"time"
)
func mintAndDownload(base, apiKey, imageID, dest string, attempt int) error {
body, _ := json.Marshal(map[string]string{"image_id": imageID})
req, _ := http.NewRequest(http.MethodPost, base+"/downloads", bytes.NewReader(body))
req.Header.Set("Authorization", "Bearer "+apiKey)
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Accept", "application/json")
res, err := (&http.Client{Timeout: 20 * time.Second}).Do(req)
if err != nil {
return err
}
defer res.Body.Close()
if res.StatusCode == 409 {
if attempt >= 8 {
return fmt.Errorf("lease busy too long")
}
wait := time.Duration(math.Min(30, 0.5*math.Pow(2, float64(attempt-1)))*1000) * time.Millisecond
time.Sleep(wait + time.Duration(rand.Intn(250))*time.Millisecond)
return mintAndDownload(base, apiKey, imageID, dest, attempt+1)
}
if res.StatusCode == 429 {
time.Sleep(5 * time.Second)
return mintAndDownload(base, apiKey, imageID, dest, attempt+1)
}
if res.StatusCode != http.StatusCreated {
b, _ := io.ReadAll(res.Body)
return fmt.Errorf("mint %d: %s", res.StatusCode, b)
}
var payload struct {
Data struct {
DownloadURL string `json:"download_url"`
} `json:"data"`
}
if err := json.NewDecoder(res.Body).Decode(&payload); err != nil {
return err
}
dl, err := (&http.Client{Timeout: 120 * time.Second}).Get(payload.Data.DownloadURL)
if err != nil {
return err
}
defer dl.Body.Close()
f, err := os.Create(dest)
if err != nil {
return err
}
defer f.Close()
_, err = io.Copy(f, dl.Body)
return err
}
// renderMockup is YOUR code — Epochal has no composite endpoint
func renderMockup(originalPath, templateID, productID string) error {
_ = originalPath
_ = templateID
_ = productID
return nil
}
func runExportQueue(base, apiKey string, jobs []struct{ ImageID, ProductID, TemplateID string }, dir string) error {
for _, job := range jobs {
dest := filepath.Join(dir, job.ImageID+".bin")
if err := mintAndDownload(base, apiKey, job.ImageID, dest, 1); err != nil {
return err
}
if err := renderMockup(dest, job.TemplateID, job.ProductID); err != nil {
return err
}
}
return nil
}
# Per job, serially:
curl -sS -X POST "https://epochalstock.com/api/v1/downloads" \
-H "Authorization: Bearer $EPOCHAL_API_KEY" \
-H "Content-Type: application/json" \
-d '{"image_id":"es_001_example"}'
# Parse data.download_url from 201 JSON, then:
curl -fL "$DOWNLOAD_URL" -o "es_001_example.bin"
# On HTTP 409: wait and retry mint. Do not parallelize on one account.
# Then run YOUR mockup CLI against the saved file.
// Application-level only
onApprove(productId, imageIds[]):
for id in unique(imageIds):
enqueue Job{ productId, imageId: id, status: "queued" }
workerLoop(): // single consumer per Epochal account
job = claimNext("queued")
job.status = "exporting"
try:
meta = optional batch/detail for extension & pixels
mint POST /downloads { image_id }
if 409: release claim → requeue with backoff; continue
if 429: sleep Retry-After; requeue; continue
if 404 / original_unavailable: job.status = "failed"; continue
GET download_url → private storage // no API key
job.status = "exported"
mockup = yourComposite(storagePath, template)
job.status = "composited"
catch:
job.status = "failed"
// Never: Promise.all(imageIds.map(mintAndDownload)) on one account
| Item | v1 value | POD note |
|---|---|---|
| Subscription | $50 USD/month | No per-download credits in v1. |
| Catalog requests | 30 / minute per account | Search, detail, batch, tags, stats, and download mints share this budget. |
| Original transfers | Unlimited serial · concurrency 1 | Throughput ≈ file size × network; schedule bulk nights if needed. |
| Binary GET | No extra catalog rate unit | Still holds the download lease until finished. |
GET /images, GET /images/{id}, POST /images/batch,
GET /tags, GET /stats, POST /downloads + token GET.Before you ship POD export
| HTTP | Code | POD meaning |
|---|---|---|
201 |
— | Token minted; download immediately without the API key. |
401 |
invalid_api_key / invalid_download_token |
Fix worker credentials or mint again. |
402 |
subscription_required / subscription_past_due |
Pause exports until billing is healthy. |
404 |
image_not_found / original_unavailable |
Fail the job permanently; notify operator. |
409 |
download_already_active |
Backoff; keep serial discipline. |
429 |
rate_limit_exceeded |
Honor Retry-After; slow mint/search traffic. |
Deep dive on two-step tokens, queues, and 409 handling.
Canonical download reference and error table.
Operator discovery with GET /images and cursor pagination.
Editorial attach flow when print is not the primary surface.