Core concepts · Costs

Costs and budgets

Esy records cost per provider call, not per run, and every number has a documented source. Budgets sit in front of execution: a run that would breach one is refused before it spends anything.

Cost is not price

What you see here is cost — what the providers charged to make the thing. It is not a price to a customer, and Esy does not compute margin for you. Every figure traces back to a model, a quantity, and a unit rate that was in effect at the time.

What one run costs

A finished run itemises its own bill. This is the real ledger from the Quickstart run:

GET /v1/runs/{run_id}json
"totalCosts": {
  "estimatedUsd": 0.007749,
  "actualUsd": 0.007749,
  "currency": "USD",
  "status": "provider_reported",
  "providerSteps": [
    { "provider": "openai",        "operation": "Render illustration",
      "model": "gpt-image-2.5-sunburst",     "actualCostUsd": 0.0046 },
    { "provider": "anthropic",     "operation": "Classify asset",
      "model": "claude-haiku-4-5-20251001",  "actualCostUsd": 0.000884 },
    { "provider": "anthropic",     "operation": "Text gate",
      "model": "claude-haiku-4-5-20251001",  "actualCostUsd": 0.00226 },
    { "provider": "cloudflare_r2", "operation": "storage.upload",
      "model": null,                          "actualCostUsd": 0.0000045 }
  ]
}

The render was $0.0046 and the two checks around it were $0.0031 together — the quality apparatus cost two-thirds of what the image did. That ratio is normal for gated workflows, and it is the thing to look at before concluding a workflow is expensive.

The three cost states

Each ledger row moves through the same three states. There are three — not four.

PROVIDER_COST_LEDGERestimatedbefore the callprovider_reportedafter the callreconciledagainst the invoicestep → run → workflow → project → workspace
Cost statesBudgets are checked against the estimate, before the provider is ever called.
StateWhenWhat it means
estimatedBefore the callPriced from the model’s published rate and an expected quantity. This is what budgets check.
provider_reportedImmediately afterThe provider told us the real quantity. Usually close to the estimate; occasionally not.
reconciledLater, against the invoiceConfirmed against what we were actually billed. The final word.
There is no “disputed” state
Older material describes a fourth state for when an estimate and a provider figure diverge. It does not exist in the API. If you have code branching on it, that branch is dead.

Reading spend

GET /v1/costs aggregates across runs. Filter by workspace, project, workflow, or period to get the rollup you need rather than summing ledgers yourself.

A collection adds one more rollup: every attempt at every member carries its own cost, so a pack reports what it cost across all the orders it placed, and a failed attempt can be refunded by its id.

Budgets

A budget is a limit attached to a scope, with a period and an enforcement mode. It is evaluated before a run executes, so a refusal costs nothing.

POST /v1/budgetsbash
curl -X POST https://api.esy.com/v1/budgets \
  -H "Authorization: Bearer $ESY_API_KEY" \
  -H "content-type: application/json" \
  -d '{
    "workspaceId": "9a1b6d4c-…",
    "name": "Clip art, monthly",
    "workflowId": "generate-clip-art-asset-v2",
    "limitUsd": 50.0,
    "period": "monthly",
    "perRunCapUsd": 0.35,
    "enforcementMode": "hard_stop"
  }'
FieldTypeRequiredDescription
workspaceIduuidrequiredEvery budget belongs to a workspace.
projectIduuidoptionalNarrows the budget to one project.
workflowIdstringoptionalNarrows it to one workflow. There is no scope field to send — the scope is inferred from which of these ids you set, and comes back on the response as workspace, project, or workflow.
limitUsdnumberrequiredThe ceiling for one period.
periodenumoptionaltotal, daily, weekly, or monthly.
perRunCapUsdnumberoptionalRefuses any single run estimated above this, regardless of remaining budget.
enforcementModeenumoptionalWhat happens at the limit — see below.

Enforcement modes

ModeAt the limitUse when
hard_stopRefuses with 402. Nothing is spent.You want a real ceiling.
allow_overageKeeps going until overageUsd is also used up.A limit with a deliberate grace margin.
allow_one_morePermits exactly one more run, then refuses.You would rather finish the item in flight than truncate it.
track_onlyNever refuses. Records everything.You want the number before you want the brake.

What a refusal looks like

POST /v1/runsjson
HTTP 402

{
  "detail": {
    "code": "budget_exceeded",
    "reason": "hard_stop",
    "budgetId": "budget-…",
    "scope": "workspace",
    "enforcementMode": "hard_stop",
    "limitUsd": 50.0,
    "spendUsd": 49.82,
    "runEstimateUsd": 0.28,
    "remainingUsd": 0.18
  }
}

The body carries everything you need to explain the refusal to a person: which budget, which rule, the limit, the spend so far, and what this run would have cost. reason is one of per_run_cap_exceeded, hard_stop, allow_overage_exceeded, or allow_one_more_exhausted.

Refusals are durable. GET /v1/budgets/{budget_id}/refusals lists what a budget turned away — useful when someone asks why a batch came back short.

A failed run still costs money
Budgets stop runs from starting. They cannot refund a run that failed at its last gate after paying for a render. When you plan spend for a gated workflow, plan for the failures too.

Estimating before you commit

For a single run, the server estimates for you at creation and refuses with a 402 if a budget would be breached — nothing is spent. For batches, use a Generation Order: it is created in the planned state with estimatedCostUsd already filled in, and the budget is checked both when you create it and again when you start it. Read the estimate, then decide — finding the limit at item 400 of 500 is an expensive way to learn it.

In short
  • Cost is recorded per provider call, with the model, quantity, and rate behind every figure.
  • Three cost states: estimatedprovider_reported reconciled.
  • Budgets refuse before execution, so a 402 is free — but a failed run is not.
  • Checks and gates are often the larger half of a workflow’s bill. Look at providerSteps before optimising the wrong thing.