App blueprint · Print / POD

Print & POD mockup source pipeline

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.

Product overview

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.

Who builds it

POD platforms, merch agencies, packaging studios, calendar/photo-book apps, and internal brand portals that feed print vendors.

Operator UX

Search + tag filters, preview grid, product board, “approve for print,” job progress (“3 of 12 originals”), mockup gallery generated by your renderer.

What Epochal provides

Licensed catalog search, metadata, tags, stats, and full-resolution originals via documented download tokens—not mockup templates or print ICC conversion.

Plan fit

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.

Architecture

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.

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
  • Use a named key such as pod-export-prod; staging keys still share the same 30 req/min and download lease.
  • Validate every image_id against jobs owned by the authenticated operator/org.
  • Store originals in private buckets; signed storefront URLs should be your CDN signatures, not Epochal mint URLs.
  • Surface serial progress in the UI—do not promise parallel original pulls on one account.

Pipeline stages (product view)

  1. Search — operator enters free text and/or tag chips; backend calls GET /images with q, tags, tag_mode, sort, limit, and optional cursor.
  2. Review previews — render thumbnail_url / preview_url and titles. Optional detail call for dimensions and description before pin.
  3. Pin / approve — store image_id on a product, campaign, or print job in your database (role: artwork, background, sleeve, etc.).
  4. Preflight metadata — before minting downloads, batch-hydrate IDs to confirm extension, pixel size, and existence (found vs requested).
  5. Serial original export — worker mints and downloads one file at a time; on 409 download_already_active, backoff and retry.
  6. Mockup & print (you) — place artwork on templates, apply bleed/safe areas, color management, and vendor file formats using your own stack.

Mockup composite is your app—not Epochal

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.

Your compositor inputs

Local path or private URL to the original you exported, template ID, placement rect, color profile, output DPI.

Your compositor outputs

Customer-facing mockup JPEGs/WebPs, plus print packages (PDF/TIFF/PNG) for fulfillment.

What not to invent

Do not document fake Epochal routes like /mockups, /print, or /composite. Keep those on your domain.

License boundary

Use originals under the Epochal Stock commercial license; do not resell the raw catalog as a competing stock library.

Serial export queue for print-ready files

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.

Recommended

Durable jobs

Database rows or a queue service with status queuedexportingexportedcomposited / failed.

Mint late

No pre-mint pile

Tokens last about five minutes and are single-use. Create the token only when the worker is ready to stream the binary.

Timeouts

Large files

Set generous client timeouts for the binary GET. A hung transfer may hold the concurrency lease.

UI

Honest progress

Show “exporting 4 of 18” rather than spinning 18 parallel progress bars that will 409.

  1. MintPOST https://epochalstock.com/api/v1/downloads with Bearer key and {"image_id":"…"}201 + download_url.
  2. TransferGET the URL immediately without the API key; write to private storage.
  3. Release — mark job complete; start the next queued image_id.
  4. Composite — invoke your mockup/print pipeline on the stored file (may run in parallel with later exports if it no longer needs the Epochal lease).

Canonical export playbook: Safe original export pipeline.

Handle 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.

Code examples

Server/worker only. Default base URL: https://epochalstock.com/api/v1. Never expose EPOCHAL_API_KEY to browsers or storefronts.

1. Search + preview proxy

cURL
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"

2. Preflight batch before export

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

3. Serial export queue (mint → GET → next)

Single-flight worker loop with 409/429 backoff. Composite step is a stub—your code, not Epochal.

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

4. Pseudocode job state machine

// 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

Rates & plan for POD traffic

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.
  • Cache tag lists and avoid search-on-every-keystroke against Epochal.
  • Batch preflight beats N detail calls when approving multi-image products.
  • Separate interactive search spikes from bulk export windows when sharing one account.
  • Extra named keys do not multiply rate limits or download concurrency.

Rate limits & errors · Security & billing.

Out of scope

  • Mockup / print rendering APIs on Epochal — your domain only.
  • AI generation endpoints — not part of Developer Image API v1.
  • Browser API keys — catalog auth is server-side Bearer only.
  • Parallel original downloads on one account — use a serial queue.
  • Invented endpoints — stick to documented GET /images, GET /images/{id}, POST /images/batch, GET /tags, GET /stats, POST /downloads + token GET.

Ship checklist

Before you ship POD export

  • Browser → your backend → Epochal; secrets only on workers/API servers.
  • Search returns previews; print uses two-step originals after approve.
  • Preflight with batch; skip missing IDs cleanly.
  • Single-flight export queue; handle 409 and 429.
  • Mint immediately before GET; five-minute single-use tokens.
  • Private storage for originals; mockups rendered by your stack.
  • UI shows serial progress; no fake parallel original promises.
  • Commercial license respected; no public anonymous stock proxy.
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.

Next steps

Asset search

Operator discovery with GET /images and cursor pagination.

CMS illustrator

Editorial attach flow when print is not the primary surface.