Authentication
Bearer format, key lifecycle, revocation, and why query-string keys fail.
Developer Image API v1
This guide walks you from a new developer account to a working authenticated request against
https://epochalstock.com/api/v1.
You will register, subscribe to the single $50 USD/month plan,
create a named API key, store it in the environment, and call health plus image search.
Before you write integration code, make sure you have:
curl for quick terminal checksBrowser JavaScript is not a supported client for authenticated calls. CORS is intentionally restricted for catalog endpoints that require a secret key. Your backend should validate input and proxy only the fields your product needs.
Account and billing operations run through the developer portal (management session + CSRF). Catalog traffic uses the Image API base URL with a Bearer key. The two surfaces are separate on purpose.
Option A — instant free trial (fastest): skip registration and billing entirely.
POST https://epochalstock.com/api/v1/trial/signup with {"email":"...","app_name":"..."}
returns a working es_test_ key immediately — 2,000 requests, 2/minute, 1 month.
Continue with step 4 below. Details on the
pricing & trial page.
The steps that follow are Option B, the paid onboarding.
Create an account with name, company, email, and a password of at least 12 characters.
Registration and login use a management session: obtain a CSRF token first, keep the session cookie,
and send X-CSRF-Token on state-changing portal requests.
The paid plan is $50 USD per month for the full image catalog, 30 requests per minute, unlimited serial original downloads, and up to 20 named API keys. First-time subscribers get 20% off the first month ($40) automatically. There is no per-image billing in v1.
Start a subscription from the portal billing flow. Approve it in PayPal. Access becomes available only after
PayPal reports the subscription as ACTIVE (webhook-driven activation). Payment failure or
suspension moves the account to past_due and blocks catalog access until resolved.
After the subscription is active, create a key with a clear project name (for example
cms-production or design-app-staging).
The full secret is shown only once at creation time. Epochal Stock stores a keyed hash, not the raw secret.
You can create up to 20 named keys per account and revoke any key immediately if a project is compromised.
Copy the secret into a secure store and expose it to your process as an environment variable.
Recommended name: EPOCHAL_API_KEY. Never commit the key, put it in a mobile app,
paste it into support tickets, or embed it in a public repository.
Call GET /health (no key) to confirm connectivity, then
GET /images with Authorization: Bearer ….
Query-string keys such as ?api_key=… are rejected (query_key_rejected)
because URLs leak into logs, proxies, and browser history.
Onboarding path
Before production traffic
EPOCHAL_API_KEY (or secrets manager) on the serverhttps://epochalstock.com/api/v1X-Request-ID / meta.request_id)Keep configuration boring and explicit. A minimal server environment looks like this (values are placeholders—never commit real secrets):
# Catalog API (server process only)
EPOCHAL_API_BASE=https://epochalstock.com/api/v1
EPOCHAL_API_KEY=es_live_your_secret_here
# Optional docs-site settings if you self-host this documentation
# EPOCHAL_CACHE_TTL=300
# SITE_BASE_PATH=
export EPOCHAL_API_BASE="https://epochalstock.com/api/v1"
export EPOCHAL_API_KEY="es_live_your_secret_here"
# Prefer your host's secret store or CI/CD encrypted variables in production
GET /health does not require a Bearer token. Use it to verify DNS, TLS, and service availability
before debugging authentication. A healthy response returns HTTP 200; degraded conditions may return 503.
curl -sS -i "https://epochalstock.com/api/v1/health"
const res = await fetch("https://epochalstock.com/api/v1/health");
console.log(res.status, await res.text());
$ch = curl_init(getenv('EPOCHAL_API_BASE') . '/health');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 10,
]);
$body = curl_exec($ch);
$status = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
echo $status, PHP_EOL, $body, PHP_EOL;
import os, requests
base = os.environ.get("EPOCHAL_API_BASE", "https://epochalstock.com/api/v1")
r = requests.get(f"{base}/health", timeout=10)
print(r.status_code, r.text)
Once EPOCHAL_API_KEY is set, list images with optional text search.
Default page size is 30 (max 100). Pass next_cursor unchanged for the next page—do not invent cursors.
curl -sS "https://epochalstock.com/api/v1/images?q=forest&sort=newest&limit=5" \
-H "Authorization: Bearer $EPOCHAL_API_KEY" \
-H "Accept: application/json"
const base = process.env.EPOCHAL_API_BASE ?? "https://epochalstock.com/api/v1";
const key = process.env.EPOCHAL_API_KEY;
if (!key) throw new Error("Set EPOCHAL_API_KEY");
const response = await fetch(
base + "/images?q=forest&sort=newest&limit=5",
{
headers: {
Authorization: "Bearer " + key,
Accept: "application/json",
},
}
);
if (!response.ok) {
throw new Error(response.status + " " + (await response.text()));
}
const body = await response.json();
console.log("total:", body.data?.total);
console.log("first title:", body.data?.images?.[0]?.title);
console.log("request_id:", body.meta?.request_id);
$base = rtrim((string) getenv('EPOCHAL_API_BASE'), '/') ?: 'https://epochalstock.com/api/v1';
$key = (string) getenv('EPOCHAL_API_KEY');
if ($key === '') {
throw new RuntimeException('Set EPOCHAL_API_KEY');
}
$url = $base . '/images?' . http_build_query([
'q' => 'forest',
'sort' => 'newest',
'limit' => 5,
]);
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 20,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . $key,
'Accept: application/json',
],
]);
$response = curl_exec($ch);
$status = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($status !== 200) {
throw new RuntimeException('HTTP ' . $status . ': ' . $response);
}
$payload = json_decode((string) $response, true, 512, JSON_THROW_ON_ERROR);
$data = $payload['data'] ?? [];
echo 'total: ', ($data['total'] ?? 0), PHP_EOL;
import os, requests
base = os.environ.get("EPOCHAL_API_BASE", "https://epochalstock.com/api/v1").rstrip("/")
key = os.environ["EPOCHAL_API_KEY"]
r = requests.get(
f"{base}/images",
params={"q": "forest", "sort": "newest", "limit": 5},
headers={
"Authorization": f"Bearer {key}",
"Accept": "application/json",
},
timeout=20,
)
r.raise_for_status()
body = r.json()
print("total:", body["data"]["total"])
print("request_id:", body["meta"]["request_id"])
Useful query parameters on GET /images:
q (max 200 characters),
tags (comma-separated, up to 20),
tag_mode (all or any),
sort (relevance, newest, oldest),
limit, and cursor.
See the Images page for the full reference.
Successful JSON responses wrap payload content in data and attach request metadata under meta.
Image list responses include images, total, the effective limit,
an opaque next_cursor when more results exist, and an echo of applied filters.
{
"data": {
"images": [
{
"id": "es_001_example",
"title": "Golden forest path",
"width": 6000,
"height": 4000,
"tags": ["nature", "trees", "forest"]
}
],
"total": 1284,
"limit": 5,
"next_cursor": "eyJvZmZzZXQiOjV9",
"filters": {
"q": "forest",
"sort": "newest"
}
},
"meta": {
"request_id": "req_01EXAMPLE"
}
}
{
"error": {
"code": "invalid_api_key",
"message": "The API key is missing, invalid, or revoked.",
"details": {},
"request_id": "req_01EXAMPLE"
}
}
Also inspect response headers on authenticated calls:
X-Request-ID — correlation ID for supportX-RateLimit-Limit — always 30 for the account windowX-RateLimit-Remaining — remaining budget in the current minuteX-RateLimit-Reset — unix timestamp when the window resetsRetry-After — present on HTTP 429 responses
Common early-integration failures: missing Authorization header (401),
inactive subscription (402), and exceeding 30 req/min (429).
Full tables live under Limits & Errors.
Never put your API key in the browser or any public frontend. Secret Bearer keys belong only on trusted servers or serverless backends with locked-down environment variables. Do not ship keys in SPA bundles, browser extensions, client-side config files, or mobile apps that end users can reverse engineer. If a key might have been exposed, revoke it in the portal and create a new named key immediately.
Downloads are designed so the secret key never appears on the binary transfer URL:
mint a five-minute token with POST /downloads, then fetch the returned
download_url without Authorization. Treat that URL as sensitive until it expires.
Details: Downloads
and Security & Billing.
You have connectivity and a first search. Continue with the guides that match your integration:
Bearer format, key lifecycle, revocation, and why query-string keys fail.
Full parameter reference for q, tags, sort, limit, and cursor pagination.
Single-image metadata and POST /images/batch for up to 100 IDs per request.
Two-step original delivery, token expiry, and the one-at-a-time concurrency rule.
Browse normalized tags and monitor catalog size with live counts.
Longer copy-paste samples for production-shaped server clients.