The dashboard at https://app.ergoda.com/ covers the everyday work: deciding
on steps, switching autopilot on and off and setting its limits, keys,
members, alerts and billing. This page is for scripts, CI jobs and reports.
It lists every other report and control as an HTTP call to
https://api.ergoda.com, with the key it needs and one example.
Calling the API
- Send a key for your site as
Authorization: Bearer $ERGODA_KEY. In the examples$ERGODA_SITEis your site id, whichergoda initprinted. The keyergoda initprinted is an agent (ingest) key and reaches none of the calls on this page except the ones the table below gives it; make a read or full key withergoda login --scope fullfor the rest. The login's browser approval names the requested scope, and the key reaches its own site only. - Times are ISO-8601 in UTC, such as
2026-09-01T00:00:00Z. Aperiodis a calendar month, such as2026-09. Asince,untilor any*_since/*_untilthat is not an ISO-8601 date or timestamp is a422. - A report without
sincecovers all the history kept for your site. When a report starts later than you asked, theX-Ergoda-Window-Sinceresponse header says where it started. - Every write names a person in
by(orapproved_by,corrected_by), and that name goes on the record. $STEP_IDin the examples is a step'sstep_cluster_id, asGET .../policyandGET .../stepsshow it. A step made withergoda init --step resolve_ticketisSITE:resolve_ticket.- Errors come back as
{"detail": "..."}:401for a missing or unknown key,403for another site or a scope the key lacks,404when nothing is published yet,422for a write that was refused, with the reason,413for a body that is too large and429for a site that is sending too fast (see Limits). - Every reply carries
Cache-Control: private, no-store.
Each key has one scope. Make keys on the dashboard's API keys page, which shows a new key once.
Limits
These bound what one key can ask of the shared service. The SDKs and the proxy
handle every one of them without an error reaching your application; they
matter to you only if you post to POST /v1/events yourself.
| Limit | Value | Answer |
|---|---|---|
| Request body | 4 MiB | 413, before the body is read |
| Events in one batch | 1,000 | 413: split the batch and send both halves |
| Events per site | 300 a second sustained, 6,000 in a burst, per server process | 429 with Retry-After in seconds: keep the batch and send it again after that |
| A site on billing hold | 402: keep the batch; it lands when the hold is released |
A batch id (batch_id) is idempotent per site: resending a batch you sent is
a no-op that answers duplicate: true, and another site using the same id
makes no difference to you.
Every string field of an event is checked against its shape: a hash field takes
lowercase hex of at most 64 characters, a name field at most 128 characters of
letters, digits and _ . : / @ + # = -, a list field at most 16 such names.
Anything else in an optional field is dropped, not refused, and the rest of the
event is stored; a malformed event_id, site_id, batch_id or timestamp is a
422.
Health check
GET https://api.ergoda.com/healthz answers {"status": "ok"} and touches no
database: it says the process is up, and it is safe to call every minute. For a
monitor that should also notice a database outage or a schema behind the
code, call GET /healthz?deep=1. It runs one query bounded to two seconds and
answers 200 {"status": "ok", "db": "ok", "schema": N}, or 503 with
{"status": "unavailable", "db": "error"} (or "timeout"), or
{"status": "degraded", "schema": N, "expected": M} when the database is
behind the code. It carries no error text and is cached for five seconds per
instance.
A recipe for an uptime monitor (Better Stack, UptimeRobot or any HTTP
monitor): one check on /healthz every 3 minutes, which keeps a scale-to-zero
database asleep, and one on /healthz?deep=1 every 10 to 15 minutes, which
wakes it; alert on two consecutive failures, and expect a short 503 with
"schema" while a deploy's migration is still running. Add a heartbeat per
scheduled job (ERGODA_HEARTBEAT_<JOB>) so a job that stops is noticed as well.
| Scope | What it reaches | Give it to |
|---|---|---|
| ingest | POST /v1/events and GET .../policy, nothing else | a serving agent |
| read | every GET on this page, and POST .../backtest | a CI job or a report |
| full | every site API call, including site key management; plan changes still require an admin | an agent operating this site's lifecycle |
ergoda login --scope full is a one-time delegation: the signed-in member
checks the code and scope in the browser, then the agent can read evidence,
publish or revert routes, run autopilot, tune sampling, manage alerts and
catalogue operations through this API.
The key remains site-scoped, is attributable to the member who granted it,
and can be revoked under API keys. It can create and revoke keys for that site;
child keys inherit the granting member's revocation linkage. It does not grant
account ownership, membership changes, payment methods, or plan changes.
An owner can refuse full-access keys for agents on the Organization page;
while refused, approving the login for that organization is refused with the
reason (and the login is denied when no organization the approver can write
to grants it), and POST /v1/sites/{site}/keys with "scope": "full" from a
key answers 403.
For machine discovery, GET /v1/sites/{site}/commands returns every
site-level command with its required fields, choices, and whether it writes.
Run one with POST /v1/sites/{site}/commands/{slug} and a JSON body of
{"values": {"field_name": "value"}}. The server supplies the key's identity
for any by field and records the operation against that key. Admin-only
fleet commands are not included. Nor are label and labels-import: a label is a person's verdict and can
license a class toward the cheap model, so no key gives one; label in the
dashboard, signed in as yourself.
curl -H "Authorization: Bearer $ERGODA_KEY" \
"https://api.ergoda.com/v1/sites/$ERGODA_SITE/commands"
curl -X POST "https://api.ergoda.com/v1/sites/$ERGODA_SITE/commands/autopilot-apply" \
-H "Authorization: Bearer $ERGODA_KEY" -H "Content-Type: application/json" \
-d '{"values": {}}'
Money
Usage for a month
The governed requests counted in a month, which is the number your bill is based on, with your plan and its prices.
Call: GET /v1/sites/{site}/billing/usage
Key: read or full
Query: period (required).
curl -H "Authorization: Bearer $ERGODA_KEY" \
"https://api.ergoda.com/v1/sites/$ERGODA_SITE/billing/usage?period=2026-09"
Invoice preview
What a month's invoice comes to so far, line by line, and the last day to
ask for a refund (refund_request_by). The amount charged so far inside the
refund window (charged_in_window_usd) includes VAT. An enterprise account
also gets its prepaid commit (commit: the amount, what has been drawn and
what remains) and a line saying so.
Call: GET /v1/sites/{site}/billing/invoice-preview
Key: read or full
Query: period (required).
curl -H "Authorization: Bearer $ERGODA_KEY" \
"https://api.ergoda.com/v1/sites/$ERGODA_SITE/billing/invoice-preview?period=2026-09"
Billing status
How your organization pays and whether it can: by invoice or by card,
whether a card is on file, and whether usage is on hold and why. It also
names the plan, the day of the month invoices are cut (invoice_day), any
open problem with the billing account (account_problems) and, for an
enterprise account, the commit and its drawdown.
Call: GET /v1/sites/{site}/billing/status
Key: read or full
curl -H "Authorization: Bearer $ERGODA_KEY" \
"https://api.ergoda.com/v1/sites/$ERGODA_SITE/billing/status"
A link to add a card
A fresh checkout link for adding a card, returned as card_url. Links
expire, so ask for one when you need it. An ingest key cannot ask, and the
deployment must have ERGODA_PUBLIC_URL set (503 otherwise): Checkout returns
the browser there with the session id.
Call: POST /v1/sites/{site}/billing/card
Key: full
Body: none.
curl -X POST -H "Authorization: Bearer $ERGODA_KEY" \
"https://api.ergoda.com/v1/sites/$ERGODA_SITE/billing/card"
Metering for any window
Governed requests in any window you choose, counted the way the bill counts
them. Requests from our own test runs are counted apart and not billed. This
report always covers the whole window you ask for. Every
environment is billed; non_production lists the requests
that came from outside production, when there are any.
Call: GET /v1/sites/{site}/metering
Key: read or full
Query: since, until.
curl -H "Authorization: Bearer $ERGODA_KEY" \
"https://api.ergoda.com/v1/sites/$ERGODA_SITE/metering?since=2026-09-01T00:00:00Z"
Plan
Which plan your organization is on: the plan field of your
settings, also on usage. Changing a plan
(POST /v1/sites/{site}/plan) takes the operator's admin token, so a site
key cannot do it.
Call: GET /v1/sites/{site}/settings
Key: read or full
curl -H "Authorization: Bearer $ERGODA_KEY" \
"https://api.ergoda.com/v1/sites/$ERGODA_SITE/settings"
Cost per customer
What one successful task costs to serve, for each of your own customers, and
what routing saved on each. A customer is the caller your agent declared on a
request (see declaring a caller), and the
report has only its digest (caller_sha256), never the id.
ergoda customers puts your ids back on your machine.
Each row has the customer's requests, tasks (one conversation each), failed
and unreported tasks, cost_usd, unpriced_requests (requests with no
recorded cost and no known price: counted, and missing from cost_usd, so a
row with any costs more than it shows), cost_per_success_usd (a task with no
outcome signal counted as a success) and cost_per_reported_success_usd
(over the tasks that reported), evidence_cost_usd (the comparison calls
that checked the routing), saving_usd and frontier_cost_usd (what the same
requests would have cost on your frontier model, priced as the dashboard
prices its saving), routed (the share a route's cheaper model answered, with
its interval) and escalation (how often a request went back to the frontier,
over the requests a person's bar governed; escalation_excluded counts the
ones under a bar autopilot moved).
callers come dearest cost per success first. A customer with fewer than
min_tasks (20) tasks is listed after the rest and marked small_sample.
Past limit customers, the rest are one row (others). Requests that named
no customer are their own row (anonymous), never part of a customer's or
of others. Only production traffic is counted.
Call: GET /v1/sites/{site}/customers
Key: read or full
Query: period, or since and until (not both); limit, 1 to 1,000
(default 50).
curl -H "Authorization: Bearer $ERGODA_KEY" \
"https://api.ergoda.com/v1/sites/$ERGODA_SITE/customers?period=2026-10"
From a terminal, with your ids and, for the gross margin, what each customer paid you that month. Both files stay on your machine:
ergoda customers --period 2026-10 --ids customers.txt --revenue revenue.csv
customers.txt has one id per line; revenue.csv has id,revenue rows in
US dollars. The margin is revenue minus cost minus the evidence cost. It
leaves out Ergoda's own fee, which is capped against the whole site's saving
for the month (billing) and so is not split by customer. If you
set ERGODA_DIGEST_KEY for your agent, set it for this command too, or put it
in the env file it reads (--env-file, default ergoda.env).
Evidence
Thresholds
Recompute every step's verdict from the evidence so far, to see why a step
is or isn't ready. Changes no route. Returns 404 until a policy is
published.
Call: GET /v1/sites/{site}/thresholds
Key: read or full
Query: since and until (default: the last 30 days; since= with no value reads all of it), min_n (default 100), floor (default 0.95), tolerance (default 0.01), persist (default false; true also records the run).
curl -H "Authorization: Bearer $ERGODA_KEY" \
"https://api.ergoda.com/v1/sites/$ERGODA_SITE/thresholds"
Corroboration
How much of the measured agreement your outcome signals back up, contradict, or leave unchecked.
Call: GET /v1/sites/{site}/corroboration
Key: read or full
Query: since, until, min_n (default 100).
curl -H "Authorization: Bearer $ERGODA_KEY" \
"https://api.ergoda.com/v1/sites/$ERGODA_SITE/corroboration?since=2026-09-01T00:00:00Z"
Commit gate
Approval, rejection, error and commit rates for each step and model in a
window. With since, it also compares the window with everything before it
and flags drift.
Call: GET /v1/sites/{site}/commit-gate
Key: read or full
Query: since, until.
curl -H "Authorization: Bearer $ERGODA_KEY" \
"https://api.ergoda.com/v1/sites/$ERGODA_SITE/commit-gate?since=2026-09-20T00:00:00Z"
Drift
Steps whose agreement with your frontier model fell between a baseline
window and a recent one. With fewer than min_n comparisons on either side,
nothing is flagged.
Call: GET /v1/sites/{site}/drift
Key: read or full
Query: baseline_since, baseline_until and recent_since (required), recent_until, min_n (default 30), delta (the drop that counts, default 0.05).
curl -H "Authorization: Bearer $ERGODA_KEY" \
"https://api.ergoda.com/v1/sites/$ERGODA_SITE/drift?baseline_since=2026-08-01T00:00:00Z&baseline_until=2026-09-01T00:00:00Z&recent_since=2026-09-01T00:00:00Z"
Escalation rate
How often steps went back to your frontier model, and why. Routes whose bar autopilot moved are reported apart and never added in.
Call: GET /v1/sites/{site}/escalation-rate
Key: read or full
Query: since, until.
curl -H "Authorization: Bearer $ERGODA_KEY" \
"https://api.ergoda.com/v1/sites/$ERGODA_SITE/escalation-rate?since=2026-09-01T00:00:00Z"
False rejects
How much of your escalation was unnecessary, and which check caused it. It counts only the steps a check refused, never your traffic as a whole.
Call: GET /v1/sites/{site}/false-rejects
Key: read or full
Query: since, until.
curl -H "Authorization: Bearer $ERGODA_KEY" \
"https://api.ergoda.com/v1/sites/$ERGODA_SITE/false-rejects?since=2026-09-01T00:00:00Z"
Canary
For one step, your outcome signals compared between the traffic each model
served. Below min_n per group it gives no number.
Call: GET /v1/sites/{site}/canary/{step_cluster_id}
Key: read or full
Query: min_n (default 30), since, until.
curl -H "Authorization: Bearer $ERGODA_KEY" \
"https://api.ergoda.com/v1/sites/$ERGODA_SITE/canary/$STEP_ID"
Discovered steps
Steps nobody has routed yet, and how often the cheaper model agreed with yours on them. A reading list, not a proposal: routing one still takes a person's approval.
Call: GET /v1/sites/{site}/discovered
Key: read or full
Query: since, until.
curl -H "Authorization: Bearer $ERGODA_KEY" \
"https://api.ergoda.com/v1/sites/$ERGODA_SITE/discovered"
Steps
For each step and model: volume, cost, latency and agreement with your
frontier model. Production traffic only, unless environment names
staging, development or eval: then the figures are that environment's
alone, and every row carries "environment" and "evidence": false. Another
word is a 422.
Call: GET /v1/sites/{site}/steps
Key: read or full
Query: since, until, environment.
curl -H "Authorization: Bearer $ERGODA_KEY" \
"https://api.ergoda.com/v1/sites/$ERGODA_SITE/steps?since=2026-09-01T00:00:00Z"
Summary
The step figures above, plus how many events are stored for your site and
the span they cover. The stored count is every environment's; the step
figures follow environment as above.
Call: GET /v1/sites/{site}/summary
Key: read or full
Query: since, until, environment.
curl -H "Authorization: Bearer $ERGODA_KEY" \
"https://api.ergoda.com/v1/sites/$ERGODA_SITE/summary"
Monthly review
The account review for one month, as JSON. The dashboard shows the same review.
Call: GET /v1/sites/{site}/review.json
Key: read or full
Query: period (required).
curl -H "Authorization: Bearer $ERGODA_KEY" \
"https://api.ergoda.com/v1/sites/$ERGODA_SITE/review.json?period=2026-08"
Dashboard
Every number on the dashboard, as JSON. /dashboard without .json
returns the same as one HTML page.
Call: GET /v1/sites/{site}/dashboard.json
Key: read or full
Query: since, until, canary_min_n (default 30).
step_savings is each step's saving over the window. pin_breaks lists
pinned steps escalating on their pin over the 24 hours to the window's end:
the step, which pin, first seen, the hashes, the saving at stake and a
sentence with the fix.
curl -H "Authorization: Bearer $ERGODA_KEY" \
"https://api.ergoda.com/v1/sites/$ERGODA_SITE/dashboard.json?since=2026-09-01T00:00:00Z"
Cost per successful task
For each routed step, what one successful task cost on the routed traffic
against the tasks the cost holdout served on the frontier model only, from
the outcomes you reported (task_outcome in
Python, taskOutcome in
TypeScript, POST /v1/task-outcomes on the
proxy).
A task is one trajectory, keyed by a digest of its turns up to the first user
message, so two runs that open with identical text are one task here.
Its outcome is the latest report for it, whenever it arrived;
conflicting_reports counts tasks reported with two different outcomes,
which is a re-judgement or two runs sharing an opening. Only success and failure are known; unknown, pending (no
report yet, last step within pending_hours) and missing (no report after
that) are counted and shown but are in no rate and no cost, and a step's
outcome flags are never read as a task's. coverage is the share of an arm's
tasks with a known outcome. The cost per success has a 95% interval over
tasks, so a long task counts once. comparison.status is insufficient, with
reasons, when either arm has fewer than 20 priced tasks with a known
outcome or no success, when under half of an arm's tasks are known, when the
two arms' coverage differs by more than 15 points, or when over 5% of an arm's
known tasks have an unpriced request. Production rows only. Nothing on this
report licenses or loosens a route.
Call: GET /v1/sites/{site}/task-outcomes
Key: read or full
Query: period (YYYY-MM) or since, until; pending_hours (0 to
2160, default 72).
curl -H "Authorization: Bearer $ERGODA_KEY" \
"https://api.ergoda.com/v1/sites/$ERGODA_SITE/task-outcomes?period=2026-10"
The dashboard's Customers page shows the same report under the per-customer table.
Model releases
Models released in the last days whose vendor serves one of the site's
steps or which the site watches, each with the site's last 30 days of
production token counts priced at the new model's price against what those
requests cost. kind is price_arithmetic on the response and on every
release: a different model writes different amounts and may need more steps,
and none of that is in the figure. Requests on your own endpoint are counted
in own_endpoint_requests and never compared. next_command is the local
ergoda evaluate run that measures the model on your recorded work.
Call: GET /v1/sites/{site}/releases
Key: read or full
Query: days (1 to 90, default 7).
curl -H "Authorization: Bearer $ERGODA_KEY" \
"https://api.ergoda.com/v1/sites/$ERGODA_SITE/releases?days=30"
Model retirement
What breaks when a provider retires a model, and which replacements already
clear the bar on your own traffic before the shutdown date. Routes that fall back to
the retiring model come first in routes, because every escalation on them
fails after the date unless you publish a new escalate_to; a route whose
cheap side is retiring can be disabled, which sends its step to the frontier.
For each step served on the retiring model directly, steps lists every
candidate already compared against it, with a state: it clears the bar, it
does not, or it is still gathering comparisons. For one still gathering, the
report says whether it will have enough in time at the current discovery rate,
and the rate that would get there with 7 days to spare. A candidate clears the
bar when the lower bound of its agreement with the retiring model clears
0.95 over at least 100 comparisons in the last 30 days.
That is agreement with the model you run now, not a quality score. Production
rows only. text is the same report as ergoda retirement prints it. The
report moves nothing; replacing a model is a person's decision.
Call: GET /v1/sites/{site}/migration
Key: read or full
Query: retiring (required, the model as your policy names it, with or
without its provider), shutdown (required, YYYY-MM-DD). A missing or
malformed value is a 422.
curl -H "Authorization: Bearer $ERGODA_KEY" \
"https://api.ergoda.com/v1/sites/$ERGODA_SITE/migration?retiring=openai/gpt-4o&shutdown=2027-02-16"
Or from a project ergoda login --read connected, with the read key it stored:
ergoda retirement openai/gpt-4o --shutdown 2027-02-16
Signed records
Attestation
The signed record of what was measured in a window, for an auditor or a
customer who asks. It comes back as document and signature. If most of
the window went unchecked, the document says so and makes no parity claim.
Call: GET /v1/sites/{site}/attestation
Key: read or full
Query: since (required), until, canary_min_n (default 30).
curl -H "Authorization: Bearer $ERGODA_KEY" \
"https://api.ergoda.com/v1/sites/$ERGODA_SITE/attestation?since=2026-09-01T00:00:00Z" \
> attestation.json
Decision record
One route's record at one policy version: the bar you registered for it and whether you registered it before the evidence, the baseline and candidate models, the window measured, the outcome rate in each arm with its interval against your margin, the coverage of each arm, what was left out and why, the agreement strata side by side, who approved the route, the machine moves on it, its review date, and a list of what the record cannot establish. A route nobody registered gets a record too, which says so and gives no verdict.
verdict is one of parity, worse, unresolved, interim (before your
planned look plus the grace period you gave late outcomes), insufficient (with the reasons), withheld (most of the
window ran under a bar autopilot loosened) or no_declared_margin.
When the bar is declared on task_failure, outcomes_by_cohort repeats the
comparison for each cohort label on your task-outcome reports, with the
trajectories no report labelled as their own row. A cohort with too little
evidence is insufficient and is never merged into another. The cohorts you
listed in the registration's cohorts are confirmatory, any other label is
exploratory, and each group's intervals are Bonferroni-corrected. A cohort
worse than your margin sets reading to keep_cohorts_on_current_model and
is named in reading_cohorts, even when the pooled verdict is parity. The
step-level outcome signals carry no cohort, so a bar declared on them has no
per-cohort section. exposure gives the route's configured exposure
(1 - baseline_share), the cap a registration declares on the step, and the
versions in which the cap raised the share. The
record is signed with the key GET /v1/policy-keys publishes, under its own
tag, so it cannot pass as an attestation. The signature says Ergoda produced
it; it is not a third-party certificate.
Call: GET /v1/sites/{site}/decision-record/{step}
Key: read or full
Query: version (default: the current policy version). {step} is a
step_cluster_id or a route's label.
curl -H "Authorization: Bearer $ERGODA_KEY" \
"https://api.ergoda.com/v1/sites/$ERGODA_SITE/decision-record/$STEP_ID" \
> decision-record.json
Pre-register a model change
Declare the bar a model change is judged on before you look at its results:
the steps, the baseline and candidate models, the outcome, the margin (the
largest added rate of that outcome per trajectory you accept), the confidence,
the coverage floor each arm must reach, the fewest trajectories per arm, the
exposure cap, the window (analysis_since_utc) and the one planned look
(look_at_utc), the review interval, and stop: alert sends an alert when
the interval sits wholly above your margin. Nothing changes the route
automatically; you revoke it.
outcome is one of two values. negative_outcome_signal (the default)
counts a trajectory as negative when your code reported a retry, a human
escalation, a correction or a tool error on any of its steps: a proxy, not
task success. task_failure counts it as negative when the latest task
outcome you reported for it (see Cost per successful task) is
failure; unknown and no report are neither success nor failure, are
counted apart, and lower the arm's coverage, and the rate is over the
trajectories with a success or failure. The record then says how many
reports arrived after the cut-off and how many never did. Either way the
outcome is your own report, and Ergoda does not verify it.
Each call writes the next version of the series and never changes an earlier one. Changing a field the analysis uses after the window opened, or withdrawing a series and starting another for the same step, marks the record post-hoc. A registration does not approve anything: publishing a route still needs its approval.
The exposure cap is enforced, and only toward your baseline model. A policy
version that raises a route on a registered step past the cap, or turns one
on above it, is refused. Once an hour, a route already above the cap has its
baseline_share raised to 1 - exposure_cap, which sends more of its
trajectories to the baseline. You get an alert, and the decision record's
window starts again at that version. Register before the window opens and
this costs the window nothing.
Call: POST /v1/sites/{site}/preregistrations (and GET to list every version)
Key: full to register, read or full to list
Body: registration_id (lower-case letters, digits and hyphens),
registered_by, and document with steps, baseline, candidate,
margin (up to 0.10), coverage_floor, min_trajectories (at least 30),
exposure_cap, analysis_since_utc, look_at_utc, review_every_days (1
to 90), and optionally outcome (negative_outcome_signal or
task_failure, default the first), confidence (default 0.95), coverage_gap_max
(default 0.05), outcome_grace_hours (default 72), stop (alert or
none), cohorts (up to 20 cohort labels whose verdicts are confirmatory,
read under task_failure), withdrawn, note, regression_runs (offline
runs of your own regression cases, written by ergoda cases attach) and
evaluations (evaluations of a candidate on your recorded states, written by
ergoda evaluate attach); both are reported, never verified, and not analysis
fields. ergoda qualify TRACE_FILE drafts this body
offline from a trace export, with every decision left as FILL IN: ...; a
body with a placeholder left is refused with 422, naming each one.
# register.json: {"registration_id": "ticket-triage", "registered_by": "Your Name",
# "document": {"steps": ["$STEP_ID"], "baseline": "anthropic/claude-sonnet-5-5",
# "candidate": "openai/gpt-6-luna", "margin": 0.02, "coverage_floor": 0.6,
# "min_trajectories": 200, "exposure_cap": 0.5,
# "analysis_since_utc": "2026-11-01T00:00:00Z", "look_at_utc": "2026-11-29T00:00:00Z",
# "review_every_days": 30}}
curl -X POST "https://api.ergoda.com/v1/sites/$ERGODA_SITE/preregistrations" \
-H "Authorization: Bearer $ERGODA_KEY" -H "Content-Type: application/json" \
-d @register.json
Audit log
Who did what to your site: sign-ins to it, keys made and revoked, members,
settings, autopilot, policy versions, account closing, recovery links, card
links, and every request our operators make with the admin token. Each row
names the actor (your colleague's verified address, key:<id>, admin, or
system:<job>), the source network (the /24 or /48, never the full address)
and a short description with no secrets in it. Oldest first, one page at a
time; pass next_after back as after for the next page.
Call: GET /v1/sites/{site}/audit
Key: read or full
Query: since, until, after, limit (default 200, at most 1,000),
action (for example key.revoke).
curl -H "Authorization: Bearer $ERGODA_KEY" \
"https://api.ergoda.com/v1/sites/$ERGODA_SITE/audit?since=2026-10-01"
One request's record
What we recorded about one answer, for the day someone asks which model
wrote it and whether anything checked it. Pass the id you logged beside the
answer: the SDK's step.request_id, or the proxy's X-Ergoda-Event header.
You get the variant and model that served it, whether it escalated and why,
the policy version and who published it, the route with the name of the
person who approved it, every gate verdict, and every other row about the
request. The answer itself never reaches us, so it is not in the record.
found is false for an unknown id, another site's id, or a request older
than events_kept_months; gate verdicts are kept for the life of the
account.
Call: GET /v1/sites/{site}/requests/{request_id}
Key: read or full
curl -H "Authorization: Bearer $ERGODA_KEY" \
"https://api.ergoda.com/v1/sites/$ERGODA_SITE/requests/$REQUEST_ID"
Or ergoda request $REQUEST_ID from a project where ergoda login ran.
Routes
Current policy
The routing policy your agents fetch right now, with its version, etag
and signature. Send the etag back in If-None-Match to get 304 when
nothing changed. Returns 404 until a policy is published. When we sign
policies, every answer, the 304 and the 404 included, also carries an
Ergoda-Policy-Fresh header: the time, the key id and an Ed25519 signature
saying which etag was current then ("" for none). A pinned SDK counts an
answer only when that signature is less than a day old, so a recorded reply
played back later is refused.
Call: GET /v1/sites/{site}/policy
Key: ingest, read or full
curl -H "Authorization: Bearer $ERGODA_KEY" \
"https://api.ergoda.com/v1/sites/$ERGODA_SITE/policy"
Policy history
Every version of the policy: who published it, and when.
Call: GET /v1/sites/{site}/policy/history
Key: read or full
curl -H "Authorization: Bearer $ERGODA_KEY" \
"https://api.ergoda.com/v1/sites/$ERGODA_SITE/policy/history"
Publish a policy
Publish a policy you wrote yourself as the next version. Every route in it
must name the person who approved it. A route carrying an autopilot
loosening your site did not authorize, or accepting more risk on
irreversible actions than your site's ceiling, is refused with 422.
Call: POST /v1/sites/{site}/policy
Key: full
Body: policy (the document, in the shape GET .../policy returns under policy) and approved_by (required), note.
# publish.json: {"policy": {...}, "approved_by": "Your Name", "note": "why"}
curl -X POST "https://api.ergoda.com/v1/sites/$ERGODA_SITE/policy" \
-H "Authorization: Bearer $ERGODA_KEY" -H "Content-Type: application/json" \
-d @publish.json
Revoke a route
Send one step back to your frontier model now. Needs nobody's approval.
Call: POST /v1/sites/{site}/policy/revoke
Key: full
Body: step_cluster_id and by (required), reason.
curl -X POST "https://api.ergoda.com/v1/sites/$ERGODA_SITE/policy/revoke" \
-H "Authorization: Bearer $ERGODA_KEY" -H "Content-Type: application/json" \
-d '{"step_cluster_id": "'"$STEP_ID"'", "by": "Your Name", "reason": "why"}'
Revert a route
Put one step back on the route it had before, as a new version.
Call: POST /v1/sites/{site}/policy/revert
Key: full
Body: step_cluster_id and by (required), note.
curl -X POST "https://api.ergoda.com/v1/sites/$ERGODA_SITE/policy/revert" \
-H "Authorization: Bearer $ERGODA_KEY" -H "Content-Type: application/json" \
-d '{"step_cluster_id": "'"$STEP_ID"'", "by": "Your Name", "note": "why"}'
Lock a step to the frontier
Keep one step on your frontier model whoever publishes next. Its routes are
disabled in the same version, and every later version that enables a route
for it (by step id or by label) is refused with 422, including one
published with an agent's full-scope key. Any full key may lock a step,
because a lock only refuses. With two_members, unlocking takes two
different people from your organization.
Only a person signed in to the dashboard can unlock a step, and no key can. Unlocking does not turn the routes back on; a route for the step needs a new publish with an approval, like any other.
Call: POST /v1/sites/{site}/policy/lock
Key: full
Body: step and by (required), note, two_members (default false).
curl -X POST "https://api.ergoda.com/v1/sites/$ERGODA_SITE/policy/lock" \
-H "Authorization: Bearer $ERGODA_KEY" -H "Content-Type: application/json" \
-d '{"step": "final_answer", "by": "Your Name", "two_members": true}'
Locked steps
Every locked step, who locked it and when, how many people unlocking it
takes, and a first request to unlock that is waiting for a second person
(it lapses after unlock_request_days).
Call: GET /v1/sites/{site}/policy/locks
Key: read or full
Retune sampling
Set how often each step is compared with your frontier model, from its
traffic, and publish those rates. Changes how much is measured, never which
model answers. A run that would change nothing returns "changed": false
and publishes no version. It also runs weekly on its own.
Call: POST /v1/sites/{site}/policy/retune-sampling
Key: full
Body: by (required), since, until.
curl -X POST "https://api.ergoda.com/v1/sites/$ERGODA_SITE/policy/retune-sampling" \
-H "Authorization: Bearer $ERGODA_KEY" -H "Content-Type: application/json" \
-d '{"by": "Your Name"}'
Writes
Write tools and what needs you
Every write tool on each routed step, with its state (validated,
acknowledged, changed, needs_decision), who acknowledged it and when,
its calls and newest call in the last 30 days, gone for a declared tool with
no calls, its share of the step's bill (bill_share) and the share that
would be freed if it were handed over (freed_share, null where the rows
carry no cost). tool is the name where your route declares one, else ""
with the digest the SDK derives from the name; ergoda writes fills in
names from your own catalogue. todo counts the rows that need a person.
Call: GET /v1/sites/{site}/writes
Key: read or full
Acknowledge a write, or re-confirm
Acknowledges tool's writes on step with no validator, or re-signs the
step's acknowledgement where one already covers it. A re-confirmation covers
every tool the acknowledgement names. tool may be "*" for every tool on
the step. It hands nothing over by itself. by is recorded with the key that
asserted it, and a by containing "(asserted via" is refused. The reply's
signed_tools lists every tool the signature now covers.
Call: POST /v1/sites/{site}/writes/acknowledge
Key: full
Body: step, tool and by (required), decision_class, note.
curl -X POST "https://api.ergoda.com/v1/sites/$ERGODA_SITE/writes/acknowledge" \
-H "Authorization: Bearer $ERGODA_KEY" -H "Content-Type: application/json" \
-d '{"step": "support.cancel", "tool": "cancel_order", "by": "Your Name"}'
Mark a tool validated
Adds tool to the step's validated_tools. Until your process passes a
validator for it, every call to it goes to your frontier.
Call: POST /v1/sites/{site}/writes/validated
Key: full
Body: step, tool and by (required), decision_class.
Withdraw an acknowledgement
Takes tool out of the step's acknowledgement. This moves toward your
frontier, so no approval is needed. What it covered goes back to the
frontier in the same version.
Call: POST /v1/sites/{site}/writes/withdraw
Key: full
Body: step, tool and by (required), reason.
Backtest
Backtest a set of routes
Score routes you have not run on the production traffic you already served: the policy in force with some routes changed, or a whole policy. It writes nothing and licenses nothing. The reply carries the candidate as a draft that publishing refuses until a person signs each route it changed.
Call: POST /v1/sites/{site}/backtest
Key: read or full (the one POST a read key may make)
Body: changes (a list of at most 50) or policy (a whole document of at
most 500 routes, checked as publishing checks it), not both; since and
until (the last 30 days by default, at most 90).
A change names a step_cluster_id and any of enabled; serve, one of the
models the policy already names and its allowlist permits (a backtest never
widens the allowlist, because a wider one moves every route serving what it
admits); and commit_confidence_threshold, from 0 to
1, or null for none. A change for a step with no route adds one and needs
enabled: true, serve and escalate_to. irreversible_alpha,
max_irreversible_commits and graduated_classes are refused, in a change or
in a whole policy, and so are irreversible_licence and graduation_licence
in a whole policy: they are licensed from evidence whose supported rate Ergoda
never quotes, and trying values against your own rows would quote it.
curl -X POST "https://api.ergoda.com/v1/sites/$ERGODA_SITE/backtest" \
-H "Authorization: Bearer $ERGODA_KEY" -H "Content-Type: application/json" \
-d '{"changes": [{"step_cluster_id": "'"$STEP_ID"'", "enabled": false}]}'
The reply:
verdict:measured;unavailable, with areasonthat says what to switch on (the policy'sexplorationwhile the traffic was served, and outcomes reported against the request); orinsufficient_databelow 100 randomised decisions with an outcome.currentandcandidate: the estimated share of requests with no bad outcome (snips), its interval (ci), how many decisions it really rests on (effective_n), the largest weight, andtrustworthy. Do not quote an estimate that is not trustworthy.coverage: the requests in the window, how many were randomised, and how many of those had an outcome.unanswerable: each step the estimate cannot speak for, with why. A step no route serves today, a model a step never ran on in the window, and a lower commit bar are never estimated: the commits a lower bar would have let through were redone by your frontier, so no outcome of them exists. A higher bar is counted (the commits it would have sent to your frontier), not estimated. A changed bar keeps the route's confidence weights, declared by whoever signs it, because a bar is compared against their sum; a whole policy that changes the weights is named here and not priced.agreement: for a step no route serves, how often the model you named agreed with yours on discovery comparisons. Agreement, not quality.restored: each route the candidate moves to another model or switches on or off while it holds something a machine moved on the model it serves now (a commit bar, an irreversible budget, graduated decision kinds), with a sentence saying so. That evidence does not describe another model, so the draft puts back what the route's owner last declared. A route that was never published with a declared bar cannot be moved here.changesandcost: per changed route, what the window would have cost more (positive) or less, priced as the dashboard prices savings, and how;unpriced_stepsnames what could not be priced.draft: the candidate document, withunsigned_routesnaming the routes whose approval record is empty: every route the candidate changed, whatever approval a whole policy carried on it. Fill in a name and time on each, then publish it.
At most 30 backtests an hour per site, then 429 with Retry-After. A window
holding more than 100,000 randomised decisions, or more than 100,000 commits
below a raised bar, is a 422: narrow it. A backtest that runs past 20
seconds is a 503.
Autopilot
Autopilot plan
What autopilot would move toward the cheaper model now, and why the rest
stays. Changes nothing. It also says whether autopilot is on for your site
and under whose limits. Returns 404 until a policy is published.
Call: GET /v1/sites/{site}/autopilot
Key: read or full
Query: since, until.
curl -H "Authorization: Bearer $ERGODA_KEY" \
"https://api.ergoda.com/v1/sites/$ERGODA_SITE/autopilot"
Run autopilot now
Apply that plan now instead of at autopilot's next daily run. A move is
published under the name that authorized autopilot (ergoda:default unless
your site set limits of its own); by records who ran it.
Call: POST /v1/sites/{site}/autopilot/run
Key: full
Body: by (required), since, until, note.
curl -X POST "https://api.ergoda.com/v1/sites/$ERGODA_SITE/autopilot/run" \
-H "Authorization: Bearer $ERGODA_KEY" -H "Content-Type: application/json" \
-d '{"by": "Your Name"}'
Switch autopilot off, or back on
The same switch as the dashboard's. Off means every move toward the cheaper model waits for a named person's approval; moves already made stay until someone reverts them. See autopilot.
Call: POST /v1/sites/{site}/autopilot
Key: full
Body: by (required) and one of: "enabled": false to switch off, "default": true to go back to the default limits, or alpha and delta (and optionally trajectory_alpha) to set limits of your own under your name. note is optional.
curl -X POST "https://api.ergoda.com/v1/sites/$ERGODA_SITE/autopilot" \
-H "Authorization: Bearer $ERGODA_KEY" -H "Content-Type: application/json" \
-d '{"by": "Your Name", "enabled": false}'
Tool catalogue
Submit your tools
Send your agent's tool definitions, in any provider's format, and get back which tools Ergoda treats as recoverable, which it withholds, and which it could not tell. No model is called. This is the one call that sends content (your tool names and descriptions), so it needs a name on it, and withdrawing deletes it.
Call: POST /v1/sites/{site}/catalogue
Key: full
Body: tools and submitted_by (required), decision_tools (the tools your route already declares as decisions).
curl -X POST "https://api.ergoda.com/v1/sites/$ERGODA_SITE/catalogue" \
-H "Authorization: Bearer $ERGODA_KEY" -H "Content-Type: application/json" \
-d '{"submitted_by": "Your Name", "tools": [{"name": "lookup_order", "description": "Read one order by id", "input_schema": {"type": "object"}}]}'
Read the catalogue
The tools your agent declared, how Ergoda treats each one, and your
corrections. Returns 404 if nothing was submitted.
Call: GET /v1/sites/{site}/catalogue
Key: read or full
curl -H "Authorization: Bearer $ERGODA_KEY" \
"https://api.ergoda.com/v1/sites/$ERGODA_SITE/catalogue"
Correct a tool
Tell Ergoda what one of your tools really does, for example that its action
can be undone. Marking a tool recoverable is the only way a withheld tool
becomes routable, so a correction carries your name.
Call: POST /v1/sites/{site}/catalogue/corrections
Key: full
Body: tool, verdict (recoverable or irreversible) and corrected_by (required), note.
curl -X POST "https://api.ergoda.com/v1/sites/$ERGODA_SITE/catalogue/corrections" \
-H "Authorization: Bearer $ERGODA_KEY" -H "Content-Type: application/json" \
-d '{"tool": "save_draft", "verdict": "recoverable", "corrected_by": "Your Name"}'
Withdraw the catalogue
Delete every catalogue your site submitted. Your corrections are kept, so the next submission does not ask the same question again.
Call: DELETE /v1/sites/{site}/catalogue
Key: full
curl -X DELETE -H "Authorization: Bearer $ERGODA_KEY" \
"https://api.ergoda.com/v1/sites/$ERGODA_SITE/catalogue"
Shared evidence
Priors
How your steps compare with evidence pooled across organizations. You see your own position and the pooled figures, never another organization's.
Call: GET /v1/sites/{site}/priors
Key: read or full
curl -H "Authorization: Bearer $ERGODA_KEY" \
"https://api.ergoda.com/v1/sites/$ERGODA_SITE/priors"
Reciprocity
What your organization adds to the shared evidence, and what it gets back.
Call: GET /v1/sites/{site}/reciprocity
Key: read or full
curl -H "Authorization: Bearer $ERGODA_KEY" \
"https://api.ergoda.com/v1/sites/$ERGODA_SITE/reciprocity"
Settings
Read settings
Your plan, whether your evidence is in the shared pool, autopilot's state
and limits, the most risk any route may accept on irreversible actions, and
your alert channel. The alert signing secret is never returned;
alerts_secret_set says whether one exists. A read key sees the webhook as
its host only (https://hooks.slack.com/…), because a chat webhook's path is
its secret; a full key sees the whole address.
Call: GET /v1/sites/{site}/settings
Key: read or full
curl -H "Authorization: Bearer $ERGODA_KEY" \
"https://api.ergoda.com/v1/sites/$ERGODA_SITE/settings"
Opt out of shared evidence
Keep your evidence out of the shared pool, or put it back.
Call: POST /v1/sites/{site}/settings
Key: full
Body: priors_opt_out (required, true or false), note.
curl -X POST "https://api.ergoda.com/v1/sites/$ERGODA_SITE/settings" \
-H "Authorization: Bearer $ERGODA_KEY" -H "Content-Type: application/json" \
-d '{"priors_opt_out": true, "note": "why"}'
Keys
Make and revoke keys on the dashboard's API keys page or with a full-scope
site key. GET /v1/sites/{site}/keys, POST /v1/sites/{site}/keys, and
POST /v1/sites/{site}/keys/{key_id}/revoke are restricted to that site's
full key or the operator's admin token. New key material is returned once.
Keys created by a full key inherit the member who granted that key, so
removing the member revokes the keys created under their grant.
What runs for you
Ergoda runs these for every site; there is nothing to call and nothing to schedule.
| What | When | Where you see it |
|---|---|---|
| Routes whose evidence fell below their bar are revoked | Daily | The route's state on the dashboard |
| Decision kinds whose audits now disagree are taken back | Hourly | The route's state on the dashboard |
| Alert conditions are checked and sent | Hourly | Your alert webhook, or email to the owner without one |
| A registered stop condition is checked | Hourly | An alert; the route is not changed |
| Models whose behaviour shifted on steps nobody reconfigured | As it happens | An alert |
| Pinned routes a deploy broke: the prompt or tool catalogue changed under the pin | Hourly | An alert, and the step on Evidence |
| The monthly review | Once the month closes | Your alert channel |
Cost per end customer, backtesting a different set of routes and what breaks when a model is retired each have their own section above (Cost per customer, Backtest a set of routes, Model retirement).
A third party can check an attestation (GET /v1/sites/{site}/attestation,
above) without trusting us. Its ed25519 block is an Ed25519 signature over
the document, made with the key published at GET /v1/policy-keys and on
Security and data. In Python, with ergoda[verify] installed:
from ergoda.signing import parse_public_keys, verify_attestation
keys, _ = parse_public_keys("<public key from the security page>")
assert verify_attestation(record, keys) is None # else: why it does not verify
The signature field beside it is an HMAC under our own secret, so only the
control plane can check it.