App blueprint · CMS illustrator

CMS & blog auto-illustrator

Editorial teams need hero images, inline figures, and Open Graph art without leaving the CMS. This blueprint shows how to build an auto-illustrator that derives search hints from the post title and body on your server, calls GET https://epochalstock.com/api/v1/images, lets the editor pick a licensed asset, and stores only the stable image_id on the content record—with optional full-resolution export for social cards via the documented two-step download flow.

Product overview

The product is a CMS plugin or headless editorial service—not a public browser client of the Image API. Authors write a draft; the illustrator panel proposes stock images that match the story; the author confirms one (or several); your database records image_id values and preview metadata for the editor UI. When you need a full-resolution file (for example an OG image pipeline or print export), a backend worker mints a download token and fetches the original serially.

Who builds it

WordPress / Drupal / Ghost plugin authors, headless CMS teams (Contentful-style apps), SaaS blog platforms, and in-house newsroom tooling.

What users see

A “Suggest images” panel next to title/body, a grid of thumbnails and titles, pick/replace controls, and optional “export original for social.”

What you store

Prefer image_id (and maybe title, dimensions, thumbnail URL snapshot) on the post. Rehydrate live metadata with detail or batch when the editor reopens.

Plan fit

One $50/month subscription, full catalog, shared 30 req/min account budget, unlimited serial originals (concurrency 1).

Non-negotiable: the browser (or CMS admin SPA) talks only to your CMS/backend. Your backend holds EPOCHAL_API_KEY and calls Epochal. Never ship a secret key in a plugin zip, theme, or admin JavaScript bundle. Authenticated catalog CORS is not a supported public-browser pattern.

Architecture: browser → CMS → Epochal

Keep secrets on the application server that already authenticates editors. Map UI actions to thin internal routes that validate session + capability, then forward documented Epochal calls.

Editor action Your route (example) Epochal surface
Suggest from draft POST /cms/illustrator/suggest GET /images with server-built q / tags
Refine search / load more GET /cms/illustrator/search GET /images + opaque cursor
Inspect one asset GET /cms/illustrator/images/{id} GET /images/{id}
Attach to post PUT /cms/posts/{id}/hero None (write your DB)
Reopen post with many IDs GET /cms/posts/{id}/media POST /images/batch
Export original for OG POST /cms/illustrator/export (worker) POST /downloads then token GET
Seed tag chips GET /cms/illustrator/tags (cached) GET /tags
Ops / health widget Internal admin only GET /stats (and public health if you already use it)
  • Store EPOCHAL_API_KEY in CMS server env / secrets manager—never in the database as a public option field visible to all admins without ACL.
  • Whitelist query parameters before calling Epochal; cap q at 200 characters and tags at 20.
  • Return only fields the editor needs (id, title, tags, dimensions, thumbnail/preview URLs).
  • Log X-Request-ID / meta.request_id on failures for support—never log the API key or full signed download URLs long-term.

End-to-end editorial flow

  1. Author drafts — title and body exist in the CMS (saved or in-progress).
  2. Suggest — editor clicks “Suggest images.” Your backend extracts keywords and optional tag hints, then calls GET /images.
  3. Browse — show watermarked previews and titles. Optional: tag chips from GET /tags (cache aggressively). Support “load more” with next_cursor.
  4. Pick — editor selects an image. Optionally call GET /images/{id} for a richer inspector (description, exact dimensions).
  5. Attach — persist image_id (and role: hero, inline, OG) on the post. Do not re-download originals on every page view.
  6. Publish render — public site can use your stored preview URL or a CDN copy you already host; for full-res OG generation, run a one-shot export job.
  7. Related posts — when listing many posts that each store an image_id, rehydrate with POST /images/batch (1–100 IDs per request).

Derive q and tags on the server

Free-text search (q) covers title, description, and tags in the catalog. Your CMS should build the query string server-side from trusted post fields—not trust a raw client-supplied string without length checks. Tag filters are optional but powerful when editors already use a controlled vocabulary that maps to Epochal tags.

Title-first q

Start with the post title (trimmed, max 200 chars). Often enough for a first suggestion page.

Body keywords

Strip HTML, drop stop-words, keep the top few distinctive tokens, and append them to q—still under 200 characters total.

CMS tags → API tags

If the post has taxonomy terms that match normalized Epochal tags, pass tags=… with tag_mode=any (broad) or all (strict).

Editor override

Always allow manual refine: the same search route accepts an explicit q and tag chips after the first automatic suggestion.

// Conceptual server helper — not an Epochal endpoint
function buildSearchHints(post):
  q = truncate(clean(post.title), 200)
  if length(q) < 12:
    q = truncate(join(topKeywords(stripHtml(post.body)), " "), 200)
  tags = mapCmsTermsToEpochalTags(post.terms)  // max 20
  tag_mode = "any" if tags.length > 1 else "all"
  return { q, tags, tag_mode, sort: "relevance", limit: 24 }

Seed chip UIs from GET /tags (each tag includes image_count). Cache tag lists; they change slowly compared with search traffic. See also Tag-driven curation.

Suggest images with GET /images

Primary discovery surface. Parameters you will actually use in an illustrator panel:

Parameter Illustrator usage
q Derived or editor-edited free text (max 200 characters).
tags + tag_mode Up to 20 tags; all or any.
sort relevance when q is set; newest for “fresh stock” browse.
limit 24–30 for editor grids (max 100).
cursor Opaque next_cursor from the previous page—never invent.

Full parameter reference: Search & list images · product-oriented search guide: Design asset search.

Editor picks and stored image_id

Once the editor confirms a selection, write a small media attachment record owned by your CMS. That record is the source of truth for templates—not a live re-search on every public page view.

Minimum fields

post_id, image_id, role (hero | inline | og), optional title / dimensions snapshot for offline admin views.

Replace / clear

Replacing a hero is a CMS write: new image_id, optional purge of a previously exported OG file in your storage.

Public pages

Prefer serving your own CDN assets or catalog preview URLs already returned by the API— do not mint download tokens on anonymous traffic.

License hygiene

Follow the Epochal Stock commercial license. Do not re-expose the catalog as a competing public stock browser with your key.

Optional OG / social originals (two-step download)

Catalog responses include thumbnails and watermarked previews only. When your social pipeline needs a full-resolution file, use the documented download pair—from a worker, not from the editor browser:

  1. MintPOST https://epochalstock.com/api/v1/downloads with Authorization: Bearer … and {"image_id":"…"} → HTTP 201 and a five-minute, single-use download_url.
  2. Transfer — plain GET on that URL without the API key. Store the bytes in private object storage; run your OG crop/resize with your own image library.

Accounts allow one concurrent original transfer. If multiple editors export at once, serialize jobs or you will see 409 download_already_active. Serial downloads are otherwise unlimited—there is no per-download credit meter in v1. Deep dive: Safe original export pipeline · Downloads reference.

  • Mint only when the worker is ready to GET—do not pre-mint tokens into a long queue.
  • Treat download_url as sensitive until used or expired; do not put it in the browser.
  • Handle 409 with backoff; handle 429 with Retry-After.
  • Skip permanent failures (image_not_found, original_unavailable) instead of tight-looping.

Batch-hydrate related posts

List views (“latest posts,” “related stories,” editorial dashboards) often hold dozens of stored image_id values. Calling GET /images/{id} once per card wastes the shared 30 req/min budget. Instead:

  • Collect unique IDs for the page.
  • Call POST https://epochalstock.com/api/v1/images/batch with {"ids":[…]}1–100 unique IDs per request, one rate unit per call.
  • Compare found vs requested; omit missing IDs gracefully in the UI.
  • For more than 100 IDs, chunk in order and space calls if you approach the rate limit.

Guide: Bulk metadata · reference: Detail & batch.

Code examples

Server-side only. Base URL defaults to https://epochalstock.com/api/v1. Load EPOCHAL_API_KEY from the environment.

1. Suggest from post title/body

Build hints, then call GET /images. Map 401/402/429 to CMS-friendly errors.

cURL
# After your CMS derives q (and optional tags) server-side:
curl -sS "https://epochalstock.com/api/v1/images?q=remote+work+desk&tags=office,technology&tag_mode=any&sort=relevance&limit=24" \
  -H "Authorization: Bearer $EPOCHAL_API_KEY" \
  -H "Accept: application/json"

2. Attach selection (CMS write) + optional detail

After pick: optional GET /images/{id}, then store image_id in your DB.

Node
const base = process.env.EPOCHAL_API_BASE ?? "https://epochalstock.com/api/v1";

async function fetchImage(imageId) {
  const res = await fetch(`${base}/images/${encodeURIComponent(imageId)}`, {
    headers: {
      Authorization: `Bearer ${process.env.EPOCHAL_API_KEY}`,
      Accept: "application/json",
    },
  });
  if (!res.ok) throw new Error(await res.text());
  return (await res.json()).data;
}

/** Editor confirmed pick — no Epochal write API; this is your CMS */
async function attachHero(postId, imageId, db) {
  const meta = await fetchImage(imageId);
  await db.posts.update(postId, {
    hero_image_id: meta.id,
    hero_title_snapshot: meta.title,
    hero_width: meta.width,
    hero_height: meta.height,
    hero_thumb_snapshot: meta.thumbnail_url,
  });
  return meta;
}

3. Batch-hydrate IDs on a related-posts list

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

4. Serial OG original export (worker)

Mint token → GET binary without key → store privately. One concurrent transfer per account.

Node
import { writeFile } from "node:fs/promises";

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 exportOriginal(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 — another original is in flight on this account
    if (attempt >= 8) throw new Error("lease busy too long");
    await sleep(Math.min(30_000, 500 * 2 ** (attempt - 1)) + Math.random() * 250);
    return exportOriginal(imageId, destPath, attempt + 1);
  }
  if (mint.status === 429) {
    await sleep(Number(mint.headers.get("Retry-After") || "5") * 1000);
    return exportOriginal(imageId, destPath, attempt + 1);
  }
  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 Authorization
  if (!fileRes.ok) throw new Error(`download ${fileRes.status}`);
  await writeFile(destPath, Buffer.from(await fileRes.arrayBuffer()));
  // Next: your OG crop/resize pipeline reads destPath
}

// Worker processes one job at a time for the account:
// await exportOriginal(job.imageId, job.destPath);

Rates, plan, and budget sharing

Item v1 value CMS implication
Price $50 USD/month One subscription for the whole product account.
Catalog API 30 req/min per account Search, detail, batch, tags, stats, and download mints share the budget.
Original downloads Unlimited serial · concurrency 1 Queue OG exports; do not fan out parallel mints.
Named keys Up to 20 e.g. cms-prod, cms-staging—still one rate limit and one download lease.
  • Cache GET /tags and GET /stats; do not poll them on every keystroke.
  • Debounce suggest calls while the author is typing; prefer explicit “Suggest” buttons.
  • Prefer batch over N detail calls on list screens.
  • Isolate heavy export windows from peak editorial search when possible.

Rate limits & errors · Security & billing.

Out of scope for Epochal v1

No AI image generation endpoints. Epochal Stock Developer Image API v1 is a licensed catalog API: search, metadata, tags, stats, and original downloads. There is no documented text-to-image, inpainting, background-removal, or “auto-generate hero” model endpoint. If your product needs generative fills, that is a separate vendor or your own model stack—do not invent Epochal paths such as /generate or /ai/*.

  • No browser-held API keys or authenticated catalog CORS for secret keys.
  • No query-string API keys (query_key_rejected).
  • No per-image credit ledger in v1 (free 1-month trial: 2,000 requests at 2/min via POST /trial/signup).
  • No video endpoints in v1.
  • Mockup compositing, CMS storage, and OG cropping are your application logic.

Ship checklist

Before you ship the illustrator panel

  • Server-only Bearer key; CMS admin UI calls your routes only.
  • Suggest builds q/tags server-side; enforce 200-char and 20-tag caps.
  • Editor can refine search and page with opaque cursor.
  • Store image_id (+ role) on the post; rehydrate with detail or batch.
  • Optional OG path uses POST /downloads + token GET on a serial worker.
  • Handle 401 / 402 / 404 / 409 / 429 with product-safe messaging.
  • No AI generation claims that imply non-existent Epochal endpoints.
  • Respect commercial license; do not run a public stock proxy for anonymous users.

Next steps

Design asset search

Deep dive on GET /images parameters, pagination, and plugin-safe proxying.

Bulk metadata

Chunked POST /images/batch for dashboards and related-post grids.

Original export

Serial queue, 409 handling, and key-safe two-step downloads.

Print & POD pipeline

When you need print-ready originals and mockups (composite stays in your app).