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
GET /v1/accountconfirmed the key and showed your credit balance.GET /v1/assetsbrowsed the public registry. Every card carries its owngenerate_url,attestation_url, and aprice_table— the exact price of a render at common presets, known before you call.POST /v1/models/{id}/generaterendered. Two things to notice:- The 202 wake ladder. A model not currently on a GPU answers
202 model_wakingwith aneta_sandRetry-Afterheader. Nothing was charged, nothing was queued — retry after the ETA. Once the model is warm, renders return200directly. If a render is taking long, you may get202 still_processingwith arequest_id: the job IS running — pollGET /v1/requests/{id}?wait=30until 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_hittells you what the request hit:live(weights already on the GPU),warm(in-memory swap, ~300 ms for SDXL),cold/frozen(the wake path).
- The 202 wake ladder. A model not currently on a GPU answers
GET /v1/models/{id}/attestationreturned a signed statement binding this endpoint to the exact Civitai model version and file hash it serves. Verify it offline.
Next
- Stack LoRAs on any base: Composition
- What a render costs and why: Serving model & pricing
- Onboard and monetize your own model: For creators