App blueprint · Agency library

Agency shared stock library

Run one licensed stock layer for many client projects: a shared library UI, per-project credentials, and careful rate budgeting. On the Developer Image API you operate one Epochal account on the single $50/month plan, create up to 20 named API keys, and accept that all keys share 30 requests per minute and one concurrent original transfer. This blueprint shows how to structure products, keys, inventory views, and ops without inventing endpoints.

Who this blueprint is for

Digital agencies

Multiple concurrent brands, each needing search, boards, and export without buying a separate stock plan per client.

In-house creative ops

Central design systems teams that expose an internal “stock library” portal to brand squads.

Platform engineers

Building multi-tenant backends that map projects to named keys, secrets managers, and shared download workers.

Not for

Reselling unlimited public API access under one key, free-tier demos, video stock, or assuming keys multiply the 30/min limit.

Product features

  • Central library UI — search (GET /images), tags, detail drawers for staff.
  • Project workspaces — your DB: client, brand, allowed users, saved ID lists / boards.
  • Named keys — e.g. client-acme-prod, client-globex-staging (max 20 total on the account).
  • Inventory stripGET /stats plus popular tags via GET /tags.
  • Shared export worker — serial POST /downloads for the whole account.
  • Usage awareness — surface X-RateLimit-* headers in ops dashboards.
  • Key rotation runbooks — revoke/create via management flows; never paste keys into Slack.

Plan facts (v1): $50 USD/month (first month $40 with the 20% upgrade discount) · full image catalog · 30 authenticated req/min per account · up to 20 named keys · unlimited serial original downloads · one concurrent original transfer · free 1-month trial (2,000 requests at 2/min via POST /trial/signup) · no per-image credits or video.

Architecture

Staff browsers and client-facing tools always stop at your backend. Which Epochal key a worker uses may depend on the project, but the key never leaves your infrastructure.

Browser / plugin UI → Your backend → Epochal API

1. Library UI SSO staff · project switcher · no raw keys
2. Agency backend Project ACL · secrets manager · rate shaping
3. Epochal API https://epochalstock.com/api/v1 Bearer per named key · shared limits
4. Project data Saved IDs, briefs, exports in YOUR DB

One account, shared ceilings. Twenty keys do not mean 20 × 30 req/min or 20 parallel originals. Budget search, batch, and mint traffic as a single pool. See Rate limits & errors.

Named keys strategy (max 20)

Keys are for isolation, rotation, and attribution—not for multiplying throughput. Give each long-lived environment a clear name when creating keys through the management/portal flow (secret shown once).

Key name (example) Purpose Guidance
agency-library-prod Primary portal + search proxy Default for interactive staff traffic
agency-export-worker Serial download minting Only workers; easier to rotate if a job host leaks
client-acme-prod Dedicated integration for one client system Revoke without touching other clients
client-globex-staging Non-prod experiments Still shares 30/min—cap automation
  • Stay at or under 20 keys; retire staging keys you no longer need
  • Map project_id → key_id / secret ref in your secrets manager—not in git
  • Prefer fewer keys with strong project ACL over one key per tiny microsite if you approach the cap
  • Log which key name was used (not the secret) alongside X-Request-ID
  • Creating more keys does not raise rate or download concurrency limits

Lifecycle, rotation, and billing context: Authentication · Security & billing.

Shared 30/min — budget carefully

Interactive designers, nightly rehydrate jobs, and export mints compete for the same 30 authenticated requests per minute. Design the agency library so background work is polite.

Interactive first

Prioritize human search latency. Pause or slow batch rehydrate jobs during business hours if headers show low remaining.

Batch, don’t N+1

Project manifests and shared boards should use POST /images/batch (≤100 IDs) instead of per-id detail storms.

Debounce & cache

Short-TTL caches for identical GET /images queries across the team save the pool.

Serial exports

One global download mutex for the account. Queue client export jobs; handle 409 download_already_active.

Workload Suggested cap
Staff search (peak) Leave ~10–15 req/min headroom for everyone else
Background batch rehydrate 1–2 req/s max bursts; back off on 429 / low remaining
Export minting Serialize; each mint is 1 catalog unit
Staging / CI Hard client-side mock when possible; never load-test against prod key

On HTTP 429 / rate_limit_exceeded, honor Retry-After and X-RateLimit-Reset. Do not spin additional keys to “get around” the limit—it will not work.

Tags & stats for inventory visibility

Agency ops and producers often ask “how big is the catalog?” and “what themes are rich enough for this pitch?” Use documented read APIs—not scraped UIs.

  • Cache GET /stats for several minutes—it rarely needs per-click freshness
  • Use tag counts to build curated “starter packs” in your CMS (stored as tag lists or ID lists)
  • Project-specific “libraries” are your saved ID sets + metadata—not Epochal /collections
  • Optional health checks: see Catalog ops

Endpoint mapping

Agency capability Your system Epochal endpoint
Global search in library Authenticated proxy GET /images
Asset inspector Proxy GET /images/{id}
Reopen client board / brief Load IDs from your DB POST /images/batch
Theme browser Proxy + cache GET /tags, GET /tags/{tag}
Catalog size widget Ops job GET /stats
Client package export Global serial worker POST /downloads + signed GET
Project / client folders Your database
Key create / revoke Portal / management API (account session) See security-billing docs—not browser catalog keys

Ops notes

Ownership

Assign a human owner for the Epochal subscription, PayPal state, and key inventory. Expired or inactive subscriptions block catalog access for every project at once.

Monitoring

Track 401/402/429 rates, X-RateLimit-Remaining percentiles, and export queue depth. Store X-Request-ID on failures.

Incident: key leak

Revoke the named key immediately, deploy a replacement secret, flush any malicious traffic patterns. Do not rotate by committing a new key to the repo.

Client offboarding

Revoke client-specific keys, archive their pin lists in your DB, and confirm the export worker no longer schedules their jobs.

Billing states, management endpoints, and the security checklist live on Security, account & billing. Rate headers and error codes: Rate limits & errors.

Code samples

Patterns below assume secrets are resolved per project from your vault, then used only on the server against https://epochalstock.com/api/v1.

Resolve project key and search

cURL
# Ops: stats snapshot (cache this)
curl -sS "https://epochalstock.com/api/v1/stats" \
  -H "Authorization: Bearer $EPOCHAL_API_KEY" \
  -H "Accept: application/json"

# Search under the same account budget
curl -sS "https://epochalstock.com/api/v1/images?q=luxury+packaging&sort=relevance&limit=24" \
  -H "Authorization: Bearer $EPOCHAL_API_KEY" \
  -H "Accept: application/json"

Inventory: stats + top tags

cURL
curl -sS "https://epochalstock.com/api/v1/stats" \
  -H "Authorization: Bearer $EPOCHAL_API_KEY"

curl -sS "https://epochalstock.com/api/v1/tags?sort=count&order=desc&limit=20" \
  -H "Authorization: Bearer $EPOCHAL_API_KEY"

Global serial export worker (account-wide)

Node
// Single worker process for the whole Epochal account — one concurrent original
const base = process.env.EPOCHAL_API_BASE ?? "https://epochalstock.com/api/v1";
const exportKey = process.env.EPOCHAL_EXPORT_KEY; // named key: agency-export-worker

async function sleep(ms) {
  return new Promise((r) => setTimeout(r, ms));
}

async function mintAndDownload(imageId, attempt = 1) {
  const mint = await fetch(\`${base}/downloads\`, {
    method: "POST",
    headers: {
      Authorization: \`Bearer ${exportKey}\`,
      "Content-Type": "application/json",
      Accept: "application/json",
    },
    body: JSON.stringify({ image_id: imageId }),
  });

  if (mint.status === 409) {
    // download_already_active — another job still transferring
    if (attempt > 10) throw new Error("lease_busy");
    await sleep(Math.min(30_000, 500 * 2 ** (attempt - 1)));
    return mintAndDownload(imageId, attempt + 1);
  }
  if (mint.status === 429) {
    await sleep(Number(mint.headers.get("Retry-After") || "5") * 1000);
    return mintAndDownload(imageId, attempt + 1);
  }
  if (!mint.ok) throw new Error(await mint.text());

  const { data } = await mint.json();
  const file = await fetch(data.download_url); // no API key
  if (!file.ok) throw new Error(\`download ${file.status}\`);
  return Buffer.from(await file.arrayBuffer());
}

// Drain a Redis/SQS queue one job at a time — never fan out across clients
async function processExportQueue(jobs) {
  for (const job of jobs) {
    const bytes = await mintAndDownload(job.imageId);
    await storePrivate(job.clientId, job.imageId, bytes);
  }
}

Security callouts

Staff portals still need a backend. An “internal-only” SPA with a live key in env is one repository leak away from total account compromise. SSO the UI; keep secrets on servers.

Do not issue Epochal keys to clients. If a client system must integrate, run a proxy under contract or create a named key that only your integration service uses—never email es_live_… to a third party chat thread.

  • Architecture always: Browser / plugin UI → Your backend → Epochal API
  • Max 20 keys; name them for blast-radius control, not throughput
  • Shared 30/min and one original transfer—design queues accordingly
  • No /collections, /upload, GraphQL, or video in v1 catalog (free 1-month trial available via POST /trial/signup)
  • Follow the full checklist on Security & billing

Build checklist

  • Confirm ACTIVE $50/mo subscription and catalog access
  • Create named keys for portal, export worker, and high-value clients (≤20)
  • Store secrets in a vault; map project → key ref in config
  • Ship authenticated search/detail/batch proxies
  • Keep client boards and “libraries” in your database as ID lists
  • Cache stats/tags for ops; watch rate-limit headers
  • One serial export worker for the account; handle 409/429
  • Write rotation and client-offboarding runbooks

Next steps

Catalog ops

Stats, health, and inventory-oriented monitoring patterns.