Your first run
Five minutes, four curl commands, one finished image. Everything on this page is a real request against api.esy.com with a real response pasted back — if a command here does not work verbatim, it is a bug in the docs.
Before you start
You need an API key. Create one in os.esy.com under Settings → API keys. The secret is shown once, at creation — Esy stores only a hash of it, so there is no way to recover it later. Put it in your shell:
export ESY_API_KEY="esy_sk_…"Step 1 — Start a run
A run is one execution of a workflow. You pick the workflow by its templateId and hand it an intake — the inputs that workflow declares. Here we use generate-illustration, which is public and needs no setup.
curl -X POST https://api.esy.com/v1/runs \
-H "Authorization: Bearer $ESY_API_KEY" \
-H "content-type: application/json" \
-d '{
"templateId": "generate-illustration",
"intake": {
"prompt": "a lighthouse at dusk, storm rolling in",
"style": "flat",
"aspectRatio": "4:3",
"quality": "low",
"categories": "landscapes"
}
}'You get 201 back immediately with status pending. Runs execute asynchronously — the API accepts the work and returns; it does not wait for the image.
{
"id": "run-fb0677b2",
"status": "pending",
"templateId": "generate-illustration",
"templateName": "Generate Illustration",
"currentStepIndex": 0,
"workflowVersion": "2026.09.09",
"specVersionHash": "sha256:eb3ced0c91a625e7ad47beaa28f5ff7b6db933ca5f6cac78fb93c1b030852fdb",
"createdVia": "api_key",
"queuedAt": "2026-09-13T00:16:58.843436Z"
}Two fields in that response matter more than they look. workflowVersion and specVersionHash are frozen at creation: this run is pinned to the exact definition that existed the moment you called it, so it stays reproducible even after the workflow is edited. createdVia: "api_key" is provenance — Esy records how every run was started.
Step 2 — Wait for it
Poll the run until it reaches a terminal status.
curl -s https://api.esy.com/v1/runs/run-fb0677b2 \
-H "Authorization: Bearer $ESY_API_KEY"Terminal statuses are completed, review, failed, and cancelled. Stop polling on any of them. Runs and steps covers the full lifecycle, including the two statuses a human review can produce.
{
"id": "run-fb0677b2",
"status": "completed",
"artifactId": "artifact-5a6a9501",
"durationMs": 16088,
"totalCosts": {
"estimatedUsd": 0.007749,
"actualUsd": 0.007749,
"currency": "USD",
"status": "provider_reported"
}
}GET /v1/runs/{run_id}/events instead. It sends a full snapshot on connect and then every state change as it happens, so you get progress rather than a spinner.Step 3 — Read the artifact
The run produced an artifact: the durable record of what was made, what it cost, and how it was checked.
curl -s https://api.esy.com/v1/artifacts/artifact-5a6a9501 \
-H "Authorization: Bearer $ESY_API_KEY"{
"id": "artifact-5a6a9501",
"runId": "run-fb0677b2",
"templateId": "generate-illustration",
"title": "Lighthouse at Dusk with Storm Rolling In",
"status": "ready",
"artifactClass": "visual",
"artifactType": "illustration",
"version": 1,
"content": {
"type": "image",
"url": "https://images.esy.com/artifacts/illustration/run-fb0677b2/image.webp",
"mimeType": "image/webp",
"model": "gpt-image-2.5-sunburst"
},
"qa": {
"status": "pending_review",
"checks": [
{ "id": "text-gate", "label": "Text gate", "status": "pass", "detail": "" }
]
}
}content.url is your image, served from images.esy.com. The qa block is the workflow’s own verdict on it — here a text gate that read the image and confirmed it carries no stray lettering.
What it cost
The run above cost $0.0077. Esy itemises that per provider call rather than giving you one opaque number:
| Step | Provider | Model | Cost |
|---|---|---|---|
| Render illustration | openai | gpt-image-2.5-sunburst | $0.004600 |
| Classify asset | anthropic | claude-haiku-4-5 | $0.000884 |
| Text gate | anthropic | claude-haiku-4-5 | $0.002260 |
| storage.upload | cloudflare_r2 | — | $0.0000045 |
Note the render is not even the majority of the bill — the two checks around it cost more than the image did. That is the trade Esy makes on your behalf, and Costs and budgets explains how to cap it.
The whole thing as a script
Start, wait, print the URL. Needs curl and jq.
#!/usr/bin/env bash
# Start a run, wait for it, print the artifact URL.
set -euo pipefail
RUN=$(curl -s -X POST https://api.esy.com/v1/runs \
-H "Authorization: Bearer $ESY_API_KEY" \
-H "content-type: application/json" \
-d '{"templateId":"generate-illustration","intake":{"prompt":"a lighthouse at dusk","style":"flat","aspectRatio":"4:3","quality":"low","categories":"landscapes"}}')
ID=$(echo "$RUN" | jq -r .id)
echo "run $ID"
# Poll until the run reaches a terminal status.
while :; do
sleep 5
STATUS=$(curl -s "https://api.esy.com/v1/runs/$ID" \
-H "Authorization: Bearer $ESY_API_KEY" | jq -r .status)
echo " $STATUS"
case "$STATUS" in completed|review|failed|cancelled) break ;; esac
done
ART=$(curl -s "https://api.esy.com/v1/runs/$ID" \
-H "Authorization: Bearer $ESY_API_KEY" | jq -r .artifactId)
curl -s "https://api.esy.com/v1/artifacts/$ART" \
-H "Authorization: Bearer $ESY_API_KEY" | jq -r .content.urlWhere to go next
| If you want to… | Read |
|---|---|
| Understand what just happened | How Esy works |
| See every workflow you can run | Workflows and catalog |
| Know what each intake field means | Intake |
| Handle failures properly | Errors |
| Generate hundreds of these | Generation Orders |
- A run is asynchronous:
POST /v1/runsreturnspending, and you poll or stream until it is terminal. - Every run pins the workflow version it executed, so results stay reproducible after the workflow changes.
- The artifact carries the output, the QA verdict, and an itemised cost ledger — not just a file.
