Skip to content

Quickstart

From API key to a rendered image, then proof of what you were served.

You need an API key (epm_live_…) — create one from your account page, or via POST /v1/keys with a session. Export it:

export EPM_API_KEY=epm_live_...

The whole quickstart is one runnable script — this exact file runs against staging in CI on every merge, so if it's in the docs, it works:

#!/bin/bash
# The quickstart, runnable end to end. This EXACT file is embedded in
# docs/quickstart.md (pymdownx.snippets) and executed against staging by
# CI — if it breaks, the build goes red, not a user's first impression.
# Requires: EPM_API_KEY. Optional: EPM_BASE_URL, EPM_GENERATE_URL.
set -euo pipefail

BASE="${EPM_BASE_URL:-https://epm-router.uridemay.workers.dev}"
AUTH="Authorization: Bearer $EPM_API_KEY"

# 1. Who am I? (sanity: the key works, shows your balance)
curl -sf -H "$AUTH" "$BASE/v1/account" | head -c 400; echo

# 2. Pick a model from the public registry (no auth needed). Every card
#    carries its own generate_url and attestation_url.
GEN_URL="${EPM_GENERATE_URL:-$(curl -sf "$BASE/v1/assets?family=sdxl&limit=1" |
  python3 -c "import json,sys; print(json.load(sys.stdin)['data'][0]['generate_url'])")}"
echo "generate_url: $GEN_URL"

# 3. Generate. A cold model answers 202 model_waking with Retry-After —
#    retry until 200. Renders are deterministic per (model, seed, params).
BODY='{"prompt": "a lighthouse at dusk, oil painting", "seed": 42,
       "params": {"steps": 25, "width": 1024, "height": 1024},
       "response_format": "b64_json"}'
for i in $(seq 1 20); do
  CODE=$(curl -s -o /tmp/epm-out.json -w "%{http_code}" -X POST \
    -H "$AUTH" -H "Content-Type: application/json" \
    -d "$BODY" "$BASE$GEN_URL")
  [ "$CODE" = "200" ] && break
  if [ "$CODE" = "202" ]; then
    ETA=$(python3 -c "import json;print(json.load(open('/tmp/epm-out.json'))['error']['details'].get('eta_s',20))")
    echo "waking (eta ${ETA}s)…"; sleep "$ETA"
  else
    echo "unexpected $CODE:"; cat /tmp/epm-out.json; exit 1
  fi
done
python3 - <<'EOF'
import base64, json
r = json.load(open("/tmp/epm-out.json"))
open("out.png", "wb").write(base64.b64decode(r["data"][0]["b64_json"]))
print("saved out.png |", r["billing"], "|", r["timing"])
EOF

# 4. Prove what you were served: the signed attestation binds the endpoint
#    to the exact Civitai source bytes. Verifiable offline forever.
curl -sf -H "$AUTH" "$BASE${GEN_URL%/generate}/attestation" | head -c 400; echo

What just happened

  1. GET /v1/account confirmed the key and showed your credit balance.
  2. GET /v1/assets browsed the public registry. Every card carries its own generate_url, attestation_url, and a price_table — the exact price of a render at common presets, known before you call.
  3. POST /v1/models/{id}/generate rendered. Two things to notice:
    • The 202 wake ladder. A model not currently on a GPU answers 202 model_waking with an eta_s and Retry-After header. Nothing was charged, nothing was queued — retry after the ETA. Once the model is warm, renders return 200 directly. If a render is taking long, you may get 202 still_processing with a request_id: the job IS running — poll GET /v1/requests/{id}?wait=30 until it completes. You are billed exactly once, at completion.
    • Determinism. Same model, seed, and params → the same bytes, every time, across pod restarts. timing.tier_hit tells you what the request hit: live (weights already on the GPU), warm (in-memory swap, ~300 ms for SDXL), cold/frozen (the wake path).
  4. GET /v1/models/{id}/attestation returned a signed statement binding this endpoint to the exact Civitai model version and file hash it serves. Verify it offline.

Next