Developer Image API · v1

Rate limits & errors

Authenticated catalog traffic is limited to 30 requests per minute per account on the paid plan and 2 per minute on the free trial (which also has a lifetime budget of 2,000 image requests). All named API keys share that budget. Responses include rate-limit headers; over-limit calls return HTTP 429 with rate_limit_exceeded. Errors use a consistent JSON shape so your server can branch on error.code without scraping free-form messages.

Overview

Plan capacity is fixed for v1: the $50 USD/month subscription (first month $40 with the 20% upgrade discount) unlocks the full catalog, up to 20 named keys, unlimited serial original downloads, and a shared 30 authenticated requests per minute ceiling. The free trial runs at 2 requests/minute with a lifetime budget of 2,000 image requests (responses carry X-Trial-Requests-Remaining). Creating more keys does not multiply the rate limit or the concurrent-download lease.

Shared budget

Every live key on the account draws from the same 30 req/min pool.

Predictable headers

X-RateLimit-Limit, Remaining, and Reset on responses.

Structured errors

error.code, message, optional details, and request_id.

Selective retries

Backoff only for 429, 502, and 503—not other 4xx.

Rate limits

The limit is 30 authenticated API requests per minute per account. Catalog endpoints that require a Bearer key (search, detail, batch, tags, stats, minting download tokens, and similar) consume this budget. Binary original transfer via a signed download_url does not consume an extra catalog request, but it is governed by the one-at-a-time concurrency lease described on Downloads.

  • Limit scope: per account, not per key and not per IP alone.
  • Extra keys share the same 30/minute ceiling—they do not stack.
  • When the budget is exhausted, the API returns HTTP 429, a Retry-After delay, and rate_limit_exceeded.
  • Design for burst-friendly patterns: batch metadata, cache hot searches, and serialize export pipelines.

Rate-limit response headers

Successful and many error responses include the current budget state. Use them to throttle proactively instead of waiting for 429.

X-RateLimit-Limit: 30
X-RateLimit-Remaining: 24
X-RateLimit-Reset: 1784498400
Header Meaning
X-RateLimit-Limit Maximum authenticated requests allowed in the current window (30).
X-RateLimit-Remaining Requests left before the limit resets.
X-RateLimit-Reset Unix timestamp when the window resets (UTC-based interpretation on the client).
Retry-After Present on 429 responses—seconds (or delay guidance) before retrying.
X-Request-ID Correlation id for support and logs on every response.

On 429: body code is rate_limit_exceeded. Honor Retry-After (or wait until X-RateLimit-Reset) with jitter. Do not spin tight loops that burn remaining budget as soon as the window opens.

Reduce request usage with batch

POST https://epochalstock.com/api/v1/images/batch accepts 1–100 unique image IDs and counts as one API request. Prefer batch when a design workflow already knows many IDs instead of issuing one GET /images/{id} per asset.

Details and examples: Image detail & batch.

Error JSON shape

Error responses use a consistent envelope. Branch on error.code for machine-readable handling; show or log message for humans; always retain request_id.

{
  "error": {
    "code": "invalid_parameter",
    "message": "q must not exceed 200 characters.",
    "details": { "parameter": "q" },
    "request_id": "req_..."
  }
}
Field Description
error.code Stable machine code (see table below).
error.message Human-readable explanation; may change wording over time.
error.details Optional structured context (parameter names, etc.).
error.request_id Same correlation id as X-Request-ID when present.

Successful JSON uses data plus meta.request_id. Do not assume error bodies appear on every non-JSON response (for example some binary download failures).

Error reference

Full catalog of documented HTTP statuses and codes for the Developer Image API v1.

HTTP Code(s) Meaning
400 invalid_json, invalid_cursor, query_key_rejected, subscription_id_required, invalid_webhook Request syntax or parameter is invalid (bad JSON, cursor, query-string key, or webhook).
401 invalid_api_key, invalid_download_token, invalid_credentials, unauthenticated, invalid_webhook_signature Authentication, session, download token, or webhook signature validation failed.
402 subscription_required, subscription_past_due The paid plan is not active or payment is past due.
402 trial_limit_reached, trial_expired Permanent until subscribed — do not retry. The trial's 2,000-request budget is used up or the trial month is over; details carries upgrade_url, a one-click upgrade_link, and the 20%-off first-month offer.
403 invalid_csrf, insufficient_scope, cors_not_supported, subscription_mismatch Security policy rejected the request (CSRF, scope, browser/CORS key use, or account mismatch).
404 not_found, image_not_found, tag_not_found, key_not_found, subscription_not_found, original_unavailable Resource does not exist or original cannot be served.
409 download_already_active, key_limit_reached, subscription_exists, email_exists, trial_already_used Resource state conflicts with the request (concurrent download, key cap, duplicate email, one trial per email, etc.).
410 upgrade_link_expired A signed upgrade link is invalid or expired; request a new one on the upgrade page.
413 payload_too_large The request or webhook body exceeds the size limit.
422 invalid_parameter, weak_password, invalid_email, invalid_name, invalid_app_name, invalid_key_name Valid JSON contains invalid field values.
429 rate_limit_exceeded, login_rate_limited, too_many_requests Too many requests; retry after the returned delay.
500 internal_error Unexpected server failure; report Request-ID to support.
502 paypal_unavailable PayPal call or verification failed (billing flows).
503 catalog_unavailable, billing_unavailable, original_unavailable Required backend, index, file, or configuration is temporarily unavailable.

Tip: Map codes to product UX carefully. 402 should route operators to billing; 409 download_already_active should pause export workers; query_key_rejected means fix the client, not retry.

Retry strategy

Retries are safe only for a small set of statuses. Blindly retrying validation or auth failures will not help and can worsen rate-limit pressure.

Retry

429 · 502 · 503

Use bounded exponential backoff with jitter. Prefer Retry-After on 429 when present. Cap total attempts.

Do not blind-retry

Most other 4xx

400, 401, 402, 403, 404, 422 usually need a code or account fix—not another identical request.

Special case

409 download_already_active

Wait for the in-flight original to finish, then retry download minting—serialize workers rather than parallel storms.

Report

500 internal_error

Log Request-ID and timestamp. A single cautious retry may be acceptable; do not hammer.

  • Set connect and read timeouts on every HTTP client.
  • Backoff formula example: min(cap, base * 2^attempt) + random_jitter.
  • Honor Retry-After as a minimum sleep when the server provides it.
  • Idempotent GETs are safer to retry than duplicate side effects; still respect rate limits.
  • Never retry with a different leaked key strategy—fix auth configuration instead.

Backoff code examples

Minimal patterns for retrying transient failures (429, 502, 503) with exponential backoff and jitter. Load secrets from the environment only.

Node
async function fetchWithBackoff(url, options = {}, maxAttempts = 5) {
  let attempt = 0;
  for (;;) {
    const res = await fetch(url, options);
    if (res.ok) return res;

    const retryable = [429, 502, 503].includes(res.status);
    if (!retryable || attempt >= maxAttempts - 1) {
      const body = await res.text();
      throw new Error(`HTTP ${res.status} id=${res.headers.get("x-request-id")} ${body}`);
    }

    const retryAfter = Number(res.headers.get("retry-after"));
    const base = Number.isFinite(retryAfter) && retryAfter > 0
      ? retryAfter * 1000
      : Math.min(30_000, 500 * 2 ** attempt);
    const jitter = Math.floor(Math.random() * 250);
    await new Promise((r) => setTimeout(r, base + jitter));
    attempt += 1;
  }
}

// Example: catalog search
const res = await fetchWithBackoff(
  "https://epochalstock.com/api/v1/images?limit=30",
  {
    headers: {
      Authorization: `Bearer ${process.env.EPOCHAL_API_KEY}`,
      Accept: "application/json",
    },
  }
);
const json = await res.json();

Why X-Request-ID matters

Every API response includes X-Request-ID. Error JSON usually mirrors it as error.request_id; successful bodies often expose meta.request_id. This is the primary correlation token for support, incident review, and multi-service logs.

Operational checklist

  • Log Request-ID on both success and failure paths for catalog and download mint calls.
  • Include Request-ID, UTC time, endpoint, and HTTP status in support tickets—never the API key.
  • When retries occur, log each attempt’s id so partial outages are visible.
  • Pair rate-limit headers with Request-ID when diagnosing unexpected 429 spikes across keys.

Do not paste secrets into tickets. A Request-ID plus approximate timestamp is enough for server-side lookup. Rotate any key that may have been exposed in logs or chat.

Next steps

Downloads

Two-step originals, concurrency lease, and token security.

Authentication

Bearer headers, key lifecycle, and auth-specific errors.