# Epochal Stock Developer Image API — Complete Reference for AI Coding Agents > Drop this file into your project (e.g. as `CLAUDE.md` section, `docs/IMAGE_API.md`, or keep the > hosted copy at https://imageapifordesignapps.com/llms-full.txt). It is self-contained: an AI > coding agent can integrate the full API — including free-trial key provisioning and the paid > upgrade flow — from this document alone. ## What this API is REST API for the Epochal Stock image catalog: search 4K stock photos by text/tags, read full metadata, and download originals. JSON only, server-side use only (a strict no-CORS policy blocks browser calls by design — never ship the API key to a browser or mobile client). - **Base URL:** `https://epochalstock.com/api/v1` - **Auth:** `Authorization: Bearer ` — keys look like `es_test_xxxxxxxx_...` (trial) or `es_live_xxxxxxxx_...` (paid). Same endpoints for both; only limits differ. - **Docs site:** https://imageapifordesignapps.com (this file: `/llms-full.txt`) - **Upgrade / pricing page:** https://imageapifordesignapps.com/upgrade.php ## Plans at a glance | | Free Trial | Developer API (paid) | |---|---|---| | Price | $0 | **$50/month** — **first month $40 (20% off)** when coming from a trial | | Duration | 1 month | until cancelled | | Total image requests | **2,000 lifetime** | unlimited | | Rate limit | **2 requests/minute** | 30 requests/minute | | Original downloads | counted toward the 2,000 budget | unlimited (1 concurrent transfer) | | API keys | 1 (issued at signup) | up to 20 named keys | The 2,000-request budget is consumed by `GET /images`, `POST /images/batch` (counts as 1), `GET /images/{id}`, and `POST /downloads`. Tag/stats endpoints do not consume it (but do count against the per-minute rate limit). ## Step 1 — Provision a free trial key (instant, no payment) `POST /trial/signup` — public, no auth, no session. One trial per email. ```bash curl -sS -X POST "https://epochalstock.com/api/v1/trial/signup" \ -H "Content-Type: application/json" \ -d '{"email":"user@example.com","app_name":"My Design App"}' ``` Response `201`: ```json { "data": { "api_key": "es_test_1a2b3c4d_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX", "key_prefix": "es_test_1a2b3c4d", "plan": "trial", "limits": { "total_requests": 2000, "requests_per_minute": 2, "expires_at": "2026-08-19T12:00:00+00:00" }, "upgrade_url": "https://imageapifordesignapps.com/upgrade.php", "docs": { "reference": "https://imageapifordesignapps.com/llms-full.txt", "site": "https://imageapifordesignapps.com" }, "notice": "Store this key now. It will not be shown again. One trial per email." }, "meta": { "request_id": "req_..." } } ``` **Agent rules for signup:** - Ask the user for their email, call this endpoint once, and store `api_key` in server-side config/env (e.g. `EPOCHAL_API_KEY`) — the key is shown exactly once. - `409 trial_already_used` → this email already has an account. Do NOT retry with invented emails. Surface `details.upgrade_url` / `details.upgrade_link` so the user can subscribe. - `429 too_many_requests` → back off; respect `Retry-After`. - `422 invalid_email` / `422 invalid_app_name` → fix input and retry once. ## Step 2 — Call the API All authenticated requests: ```http Authorization: Bearer es_test_1a2b3c4d_XXXX... Accept: application/json ``` Success envelope: `{"data": {...}, "meta": {"request_id": "req_..."}}` Error envelope: `{"error": {"code": "...", "message": "...", "details": {...}, "request_id": "req_..."}}` ### GET /health — no auth `{"status":"ok","database":"ok","catalog":"ok","catalog_built_at":"..."}` (`503` when degraded). ### GET /images — search and list | Param | Type | Default | Notes | |---|---|---|---| | `q` | string ≤200 chars | — | full-text over title/description/tags | | `tags` | comma-separated, ≤20 | — | normalized lowercase tags | | `tag_mode` | `all` \| `any` | `all` | | | `sort` | `relevance` \| `newest` \| `oldest` | `relevance` with `q`, else `newest` | | | `limit` | 1–100 | 30 | | | `cursor` | opaque string | — | pass `next_cursor` back verbatim; never construct cursors | ```bash curl -sS "https://epochalstock.com/api/v1/images?q=golden+forest&tags=nature&sort=newest&limit=30" \ -H "Authorization: Bearer $EPOCHAL_API_KEY" ``` `data`: `{ "images": [...], "total": n, "limit": n, "next_cursor": "...|null", "filters": {...} }`. Each image: `id`, `slug`, `title`, `description`, `collection`, `width`, `height`, `file_size`, `file_ext`, `tags[]`, `created_at`, `thumbnail_url`, `preview_url` (watermarked). ### GET /images/{image_id} — single image (flat `data.{...}`, full metadata) `404 image_not_found` when absent. ### POST /images/batch — up to 100 IDs, counts as ONE request (trial-friendly) ```bash 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"]}' ``` `data`: `{ "images": [...], "requested": n, "found": n }` — missing IDs are silently omitted. **On a trial, prefer batch over N single GETs: it costs 1 of the 2,000 requests instead of N.** ### GET /tags — list tags (`q`, `sort=name|count`, `order=asc|desc`, `limit` ≤100, integer `cursor`) `data`: `{ "tags": [{"tag":"forest","image_count":123}, ...], ... }` ### GET /tags/{url_encoded_tag} — flat `data.{tag,image_count,images_url}`; 404 `tag_not_found` ### GET /stats — `{ "total_images": n, "total_tags": n, "catalog_built_at": "..." }` ### Original downloads — two-step flow 1. `POST /downloads` body `{"image_id":"es_001_example"}` → `201` with a 5-minute single-use `download_url` (+ `expires_at`). Requires scope `downloads:create` (default on all keys). 2. `GET ` (no auth header) → image binary. One concurrent transfer per account; a parallel attempt returns `409 download_already_active` — wait and retry. Never log or persist `download_url`; treat it as a secret until it expires. ## Rate limits and headers Every authenticated response carries: ```http X-RateLimit-Limit: 2 # 2 on trial, 30 on paid X-RateLimit-Remaining: 1 X-RateLimit-Reset: 1784498400 # unix time the 60s window resets ``` Trial responses on counted endpoints also carry `X-Trial-Requests-Remaining: ` — surface this to users so the 2,000 budget is never a surprise. **Retry policy (important for agents):** - Retry with backoff ONLY on `429`, `502`, `503` (honor `Retry-After`). - NEVER retry `402` errors (`trial_limit_reached`, `trial_expired`, `subscription_required`) — they are permanent until the user subscribes. Retrying loops forever. Show the upgrade offer from `details` instead. ## Error reference (complete) | HTTP | Code | Meaning / agent action | |---:|---|---| | 400 | `invalid_json`, `invalid_cursor`, `query_key_rejected` | Fix the request. Keys never go in URLs. | | 401 | `invalid_api_key`, `invalid_download_token`, `invalid_credentials` | Key/token invalid or revoked. | | 402 | `trial_limit_reached` | 2,000-request trial budget used up. Permanent → show upgrade offer, do not retry. | | 402 | `trial_expired` | 1-month trial ended. Permanent → show upgrade offer, do not retry. | | 402 | `subscription_required`, `subscription_past_due` | Paid plan not active. | | 403 | `invalid_csrf`, `insufficient_scope`, `cors_not_supported` | Security policy rejected the request. | | 404 | `not_found`, `image_not_found`, `tag_not_found`, `original_unavailable` | Resource missing. | | 409 | `trial_already_used` | One trial per email → offer upgrade. | | 409 | `download_already_active`, `key_limit_reached`, `subscription_exists` | State conflict. | | 410 | `upgrade_link_expired` | Signed upgrade link expired → send user to `details.upgrade_url`. | | 422 | `invalid_parameter`, `invalid_app_name`, `weak_password`, `invalid_email` | Fix fields. | | 429 | `rate_limit_exceeded`, `too_many_requests`, `login_rate_limited` | Back off per `Retry-After`. | | 500 | `internal_error` | Report `request_id`. | | 502 | `paypal_unavailable` | PayPal hiccup — retry later. | | 503 | `catalog_unavailable`, `billing_unavailable` | Backend/config unavailable. | Every trial-related error (`trial_*`, trial `rate_limit_exceeded`) includes these `details`: ```json { "upgrade_url": "https://imageapifordesignapps.com/upgrade.php", "upgrade_link": "https://epochalstock.com/api/v1/billing/upgrade?t=", "upgrade_endpoint": "POST https://epochalstock.com/api/v1/billing/subscription with your Bearer API key, then open approve_url", "docs_url": "https://imageapifordesignapps.com/llms-full.txt", "offer": { "description": "20% off your first month when you subscribe", "discount_percent": 20, "first_month_price": "40.00", "regular_price": "50.00", "currency": "USD" } } ``` - `upgrade_link` is a personalized, signed URL (valid 7 days) that 302-redirects straight to PayPal checkout — the fastest path: show it to the user as a clickable link. - `upgrade_url` is the human landing page with the same offer. ## Step 3 — Upgrade to paid, entirely via API When the trial hits a `402`, offer the upgrade. Three equivalent paths; pick one: **A. One-click link (simplest):** display `details.upgrade_link` from the error. User clicks → PayPal checkout ($40 first month, then $50/month) → done. **B. Programmatic (for in-app flows):** ```bash # Works with the trial key even when the trial is expired or capped: curl -sS -X POST "https://epochalstock.com/api/v1/billing/subscription" \ -H "Authorization: Bearer $EPOCHAL_API_KEY" -H "Content-Type: application/json" -d '{}' ``` Response `201`: `{ "subscription": { "subscription_id": "I-...", "status": "pending", "approve_url": "https://www.paypal.com/...", "price": "50.00", "currency": "USD", "discount_applied": true, "first_month_price": "40.00" } }` Open `approve_url` in the user's browser (the only unavoidable browser step — PayPal consent). Then poll until active (webhook-driven, usually seconds after approval): ```bash curl -sS "https://epochalstock.com/api/v1/billing/status" -H "Authorization: Bearer $EPOCHAL_API_KEY" # -> data.account.plan becomes "paid", data.account.status "active" ``` The SAME key keeps working — limits lift automatically (30/min, no request cap). No re-issuance needed. Optional body `{"plan":"regular"}` skips the discount (full price from month one). **C. Landing page:** send the user to `https://imageapifordesignapps.com/upgrade.php`; they enter their trial email and are forwarded to PayPal. **Optional portal claim:** to manage keys/billing in a browser later, register on the portal with the SAME email (`GET /portal/csrf` → `POST /portal/register` with the CSRF token, email, name, 12+ char password). This claims the trial account — keys and counters are kept. ## Management endpoints (session + CSRF, for completeness) | Method | Path | Purpose | |---|---|---| | GET | `/portal/csrf` | Session + CSRF token (send as `X-CSRF-Token`) | | POST | `/portal/register` | Register (claims an existing trial account with the same email) | | POST | `/portal/login` / `/portal/logout` | Session management | | GET | `/account` | Account + plan (trial accounts include usage/limits/expiry) | | GET/POST | `/keys`, DELETE `/keys/{id}` | List/create/revoke keys (create requires paid plan) | | POST | `/billing/subscription` | Also session-capable (see Step 3 B) | | POST | `/billing/cancel` | Stop renewal (access until period end) | | GET | `/billing/status` | Session or Bearer | | GET | `/usage` | Daily per-endpoint usage; trials include `trial.requests_used/requests_limit/expires_at` | | POST | `/billing/upgrade-link` | `{email}` → personalized PayPal upgrade link (used by the landing page) | | GET | `/billing/upgrade?t=...` | Signed-token 302 redirect to PayPal checkout | ## Integration recipe for open-source apps (copy this pattern) 1. **Config:** read the key from `EPOCHAL_API_KEY` (env) or your app's server-side config store. 2. **First run / missing key:** prompt the user for an email (explain: "free trial, 2,000 requests, 1 month, one trial per email"), call `POST /trial/signup`, persist the returned key. 3. **Every call:** send the Bearer header server-side only. Respect `X-RateLimit-*`; on a trial, throttle yourself to ≤2 requests/minute and prefer `/images/batch`. 4. **Error handling:** `429` → wait `Retry-After` and retry; `402` → stop, render the offer from `details.offer` with `details.upgrade_link` as the call-to-action; `401` → ask the user to re-provision or check the key. 5. **Upgrade:** after PayPal approval the same key is unlimited — no code changes. 6. **Security:** never commit the key, never send it to the browser, never put it in URLs (`?api_key=` is actively rejected), set 20s timeouts, and rotate keys after suspected leaks. ## License Commercial license at https://epochalstock.com/license/ applies. Images licensed during an active subscription remain licensed after cancellation. Do not redistribute originals as standalone files or power a competing stock marketplace.