Evals
Test an agent against real situations, including ones it has already been in.
An eval suite is a set of scenarios and how they are scored. A run places a real call per scenario against the agent, with a tester on the other end improvising towards a goal, and scores the transcript against the suite's criteria. A scripted caller only ever tests the path someone already thought of, which is why a scenario is a goal and a persona rather than a script.
The scenarios worth having come from production. Pick a source, look at real completed calls with the sentiment and resolution the analysis pipeline already found, import the interesting ones, and let triage work out what scenario each would make. Triage is batched and resumable because it is a model call per transcript.
Runs are asynchronous: starting one returns a run at pending. Poll it, or subscribe to eval.run.completed and be told.
List suites#
Eval suites for an agent.
/api/v1/agents/{agentId}/evalsPath parameters
agentIdstringrequired- Agent id.
Responses
The suites.
curl -X GET "https://voice.evryhealth.com/api/v1/agents/{agentId}/evals" \-H "Authorization: Bearer YOUR_API_KEY"
{"data": {"evals": [{"id": "9d02…","name": "Pharmacy follow-up","status": "ready","iterationsPerScenario": 3,"scoringCriteria": [{"key": "accuracy","name": "Accuracy","weight": 0.4}],"runCount": 12,"latestRunScore": 8.1}]},"meta": {"requestId": "req_…"}}
Create a suite#
A suite starts empty; add scenarios by hand or import them from real calls.
/api/v1/agents/{agentId}/evalsPath parameters
agentIdstringrequired- Agent id.
Body
application/jsonname- string
- required
description- string
evalType- voice | text
iterationsPerScenario- number
maxCallDurationSeconds- number
Responses
The created suite.
curl -X POST "https://voice.evryhealth.com/api/v1/agents/{agentId}/evals" \-H "Authorization: Bearer YOUR_API_KEY" \-H "Content-Type: application/json" \-d '{"name": "Pharmacy follow-up","iterationsPerScenario": 3}'
{"data": {"id": "9d02…","name": "Pharmacy follow-up","status": "draft"},"meta": {"requestId": "req_…"}}
Get a suite#
One suite, including its scoring criteria and its latest score.
/api/v1/agents/{agentId}/evals/{evalId}Path parameters
agentIdstringrequired- Agent id.
evalIdstringrequired- Suite id.
Responses
The suite.
curl -X GET "https://voice.evryhealth.com/api/v1/agents/{agentId}/evals/{evalId}" \-H "Authorization: Bearer YOUR_API_KEY"
{"data": {"id": "9d02…","name": "Pharmacy follow-up","status": "ready","latestRunScore": 8.1},"meta": {"requestId": "req_…"}}
Update a suite#
Change a suite's settings. `noise` puts the tester's line under background sound; pass null to switch it off.
/api/v1/agents/{agentId}/evals/{evalId}Path parameters
agentIdstringrequired- Agent id.
evalIdstringrequired- Suite id.
Body
application/jsonname- string
description- string
iterationsPerScenario- number
maxCallDurationSeconds- number
status- draft | ready | archived
noise- { type: cafe|street|static|office, level: light|moderate|heavy } | null
Responses
The updated suite.
curl -X PATCH "https://voice.evryhealth.com/api/v1/agents/{agentId}/evals/{evalId}" \-H "Authorization: Bearer YOUR_API_KEY" \-H "Content-Type: application/json" \-d '{"status": "ready","noise": {"type": "office","level": "moderate"}}'
{"data": {"id": "9d02…","status": "ready"},"meta": {"requestId": "req_…"}}
Delete a suite#
Remove a suite and everything under it.
/api/v1/agents/{agentId}/evals/{evalId}Path parameters
agentIdstringrequired- Agent id.
evalIdstringrequired- Suite id.
Responses
Deleted.
curl -X DELETE "https://voice.evryhealth.com/api/v1/agents/{agentId}/evals/{evalId}" \-H "Authorization: Bearer YOUR_API_KEY"
{"data": {"deleted": true},"meta": {"requestId": "req_…"}}
Add a scenario#
A situation to put the agent in, expressed as a goal the tester pursues.
/api/v1/agents/{agentId}/evals/{evalId}/scenariosPath parameters
agentIdstringrequired- Agent id.
evalIdstringrequired- Suite id.
Body
application/jsonname- string
- required
description- string
goal- string
- required
persona- string
expectedOutcome- string
context- string
category- happy_path | edge_case | error_handling | adversarial
Responses
The created scenario.
curl -X POST "https://voice.evryhealth.com/api/v1/agents/{agentId}/evals/{evalId}/scenarios" \-H "Authorization: Bearer YOUR_API_KEY" \-H "Content-Type: application/json" \-d '{"name": "Prescription not ready","persona": "A busy pharmacy technician","goal": "Say the prescription is not ready and refuse to give a date.","category": "edge_case"}'
{"data": {"id": "31fa…","name": "Prescription not ready"},"meta": {"requestId": "req_…"}}
Import sources#
Campaigns and call centers whose real calls can become scenarios.
/api/v1/agents/{agentId}/evals/{evalId}/imports/sourcesPath parameters
agentIdstringrequired- Agent id.
evalIdstringrequired- Suite id.
Responses
The sources.
curl -X GET "https://voice.evryhealth.com/api/v1/agents/{agentId}/evals/{evalId}/imports/sources" \-H "Authorization: Bearer YOUR_API_KEY"
{"data": {"outboundCampaigns": [{"type": "campaign","id": "a1…","name": "September refill reminders","direction": "outbound","completedCalls": 812,"lastCallAt": "2026-09-30T17:42:10.000Z","workspaceName": "Pharmacy"}],"inboundCampaigns": [],"callCenters": [{"type": "call_center","id": "cc1…","name": "Member services","direction": null,"completedCalls": 4302,"lastCallAt": "2026-10-01T09:15:03.000Z","workspaceName": null}]},"meta": {"requestId": "req_…"}}
Candidate calls#
Completed calls from one source that could become scenarios, with the sentiment, intent and resolution already found, so you can take the unresolved and the unhappy rather than a random sample. Calls already imported into this suite are still listed, with `alreadyImported` set, so pages stay stable while you import. An unreadable `from`, `to` or `direction` is refused rather than ignored.
/api/v1/agents/{agentId}/evals/{evalId}/imports/candidatesPath parameters
agentIdstringrequired- Agent id.
evalIdstringrequired- Suite id.
Query parameters
sourceTypestringrequired- campaign or call_center.
- Example
campaign sourceIdstringrequired- The source's id.
fromstring- ISO 8601 lower bound, inclusive.
tostring- ISO 8601 upper bound, exclusive.
minDurationnumber- Seconds.
directionstring- inbound or outbound.
searchstring- Free text.
pagenumber- Page number.
- Default
1 pageSizenumber- Up to 100.
- Default
25
Responses
A page of candidates.
curl -X GET "https://voice.evryhealth.com/api/v1/agents/{agentId}/evals/{evalId}/imports/candidates" \-H "Authorization: Bearer YOUR_API_KEY"
{"data": {"candidates": [{"id": "5f1c…","externalRef": "8b7e…","direction": "outbound","phoneNumber": "+15551234567","contactName": "Dana Reyes","durationSeconds": 184,"startedAt": "2026-09-29T15:02:44.000Z","endReason": "hangup","sentiment": "negative","callerIntent": "prescription_status","resolved": false,"alreadyImported": false}],"total": 41,"page": 1,"pageSize": 25},"meta": {"requestId": "req_…"}}
List scenarios#
Every scenario in a suite, written or imported.
/api/v1/agents/{agentId}/evals/{evalId}/scenariosPath parameters
agentIdstringrequired- Agent id.
evalIdstringrequired- Suite id.
Responses
The scenarios.
curl -X GET "https://voice.evryhealth.com/api/v1/agents/{agentId}/evals/{evalId}/scenarios" \-H "Authorization: Bearer YOUR_API_KEY"
{"data": {"scenarios": [{"id": "31fa…","name": "Prescription not ready","goal": "Say the prescription is not ready and refuse to give a date.","category": "edge_case","isEnabled": true,"isAutoGenerated": false}]},"meta": {"requestId": "req_…"}}
List imports#
Batches of real calls pulled into this suite, and how far triage has got. `status` is pending, triaging, ready or failed.
/api/v1/agents/{agentId}/evals/{evalId}/importsPath parameters
agentIdstringrequired- Agent id.
evalIdstringrequired- Suite id.
Responses
The imports.
curl -X GET "https://voice.evryhealth.com/api/v1/agents/{agentId}/evals/{evalId}/imports" \-H "Authorization: Bearer YOUR_API_KEY"
{"data": {"imports": [{"id": "77c0…","source": "production","label": "September refill reminders","status": "ready","totalCalls": 18}]},"meta": {"requestId": "req_…"}}
Import calls#
Record which calls to read, up to 300. If any id is not a completed call of the source, the whole import is refused with a 422 whose `details.rejectedCallIds` names them.
/api/v1/agents/{agentId}/evals/{evalId}/importsPath parameters
agentIdstringrequired- Agent id.
evalIdstringrequired- Suite id.
Body
application/jsonsourceType- campaign | call_center
- required
sourceId- string
- required
callIds- string[] (1 to 300)
- required
Responses
The import.
curl -X POST "https://voice.evryhealth.com/api/v1/agents/{agentId}/evals/{evalId}/imports" \-H "Authorization: Bearer YOUR_API_KEY" \-H "Content-Type: application/json" \-d '{"sourceType": "campaign","sourceId": "a1…","callIds": ["5f1c…","6a2d…"]}'
{"data": {"id": "77c0…","status": "pending","totalCalls": 2,"triagedCalls": 0},"meta": {"requestId": "req_…"}}
Triage a batch#
Read the next batch of imported calls and work out what scenario each would make. Call it until `remaining` is zero; each batch commits, so a retry resumes rather than restarts. An empty body uses the defaults; an invalid one is refused.
/api/v1/agents/{agentId}/evals/{evalId}/imports/{importId}/triagePath parameters
agentIdstringrequired- Agent id.
evalIdstringrequired- Suite id.
importIdstringrequired- Import id.
Body
application/jsonbatchSize- number (1 to 20, default 6)
retryFailed- boolean
Responses
Progress.
curl -X POST "https://voice.evryhealth.com/api/v1/agents/{agentId}/evals/{evalId}/imports/{importId}/triage" \-H "Authorization: Bearer YOUR_API_KEY" \-H "Content-Type: application/json" \-d '{"batchSize": 6}'
{"data": {"import": {"id": "77c0…","triagedCalls": 6},"processed": 6,"remaining": 12},"meta": {"requestId": "req_…"}}
Review triaged calls#
What triage found for each imported call (the summary, the category, and the scenario it suggests) so it can be reviewed before anything is created.
/api/v1/agents/{agentId}/evals/{evalId}/imports/{importId}/scenariosPath parameters
agentIdstringrequired- Agent id.
evalIdstringrequired- Suite id.
importIdstringrequired- Import id.
Responses
The imported calls.
curl -X GET "https://voice.evryhealth.com/api/v1/agents/{agentId}/evals/{evalId}/imports/{importId}/scenarios" \-H "Authorization: Bearer YOUR_API_KEY"
{"data": {"calls": [{"id": "b1…","triageStatus": "done","triage": {"category": "edge_case","summary": "Pharmacy would not commit to a date.","suggestedScenario": {"name": "Prescription not ready"}},"scenarioId": null}]},"meta": {"requestId": "req_…"}}
Create scenarios from calls#
Turn triaged calls into scenarios. A call already turned into one is skipped rather than duplicated. GET the same path first to review what triage found.
/api/v1/agents/{agentId}/evals/{evalId}/imports/{importId}/scenariosPath parameters
agentIdstringrequired- Agent id.
evalIdstringrequired- Suite id.
importIdstringrequired- Import id.
Body
application/jsoncallIds- string[]
- required
Responses
What was created.
curl -X POST "https://voice.evryhealth.com/api/v1/agents/{agentId}/evals/{evalId}/imports/{importId}/scenarios" \-H "Authorization: Bearer YOUR_API_KEY" \-H "Content-Type: application/json" \-d '{"callIds": ["b1…","b2…"]}'
{"data": {"created": 2,"scenarioIds": ["31fa…","31fb…"]},"meta": {"requestId": "req_…"}}
Start a run#
Place a call per enabled scenario per iteration. Returns immediately; the run takes minutes. A suite with no enabled scenarios is refused with a 409.
/api/v1/agents/{agentId}/evals/{evalId}/runsPath parameters
agentIdstringrequired- Agent id.
evalIdstringrequired- Suite id.
Responses
The run, at pending.
curl -X POST "https://voice.evryhealth.com/api/v1/agents/{agentId}/evals/{evalId}/runs" \-H "Authorization: Bearer YOUR_API_KEY"
{"data": {"id": "e91b…","runNumber": 13,"status": "pending","totalIterations": 9},"meta": {"requestId": "req_…"}}
List runs#
Every run of a suite, newest first.
/api/v1/agents/{agentId}/evals/{evalId}/runsPath parameters
agentIdstringrequired- Agent id.
evalIdstringrequired- Suite id.
Responses
The runs.
curl -X GET "https://voice.evryhealth.com/api/v1/agents/{agentId}/evals/{evalId}/runs" \-H "Authorization: Bearer YOUR_API_KEY"
{"data": {"runs": [{"id": "e91b…","runNumber": 13,"status": "completed","agentVersion": 8,"overallScore": 8.1}]},"meta": {"requestId": "req_…"}}
Get a run#
One run, with the per-criterion breakdown behind its headline score. A score with no breakdown says nothing about what to fix.
/api/v1/agents/{agentId}/evals/{evalId}/runs/{runId}Path parameters
agentIdstringrequired- Agent id.
evalIdstringrequired- Suite id.
runIdstringrequired- Run id.
Responses
The run and its aggregates.
curl -X GET "https://voice.evryhealth.com/api/v1/agents/{agentId}/evals/{evalId}/runs/{runId}" \-H "Authorization: Bearer YOUR_API_KEY"
{"data": {"run": {"id": "e91b…","status": "completed","agentVersion": 8,"overallScore": 8.1,"completedIterations": 9,"failedIterations": 0},"aggregates": {"overallScore": 8.1,"criterionAggregates": [{"criterionKey": "accuracy","criterionName": "Accuracy","avgScore": 8.6,"minScore": 7,"maxScore": 10,"stdDev": 1.1,"weight": 0.4}],"totalResults": 9,"completedResults": 9,"failedResults": 0,"avgDurationSeconds": 102.4,"totalCost": 1.87}},"meta": {"requestId": "req_…"}}
Delete a run#
Remove a run and its results.
/api/v1/agents/{agentId}/evals/{evalId}/runs/{runId}Path parameters
agentIdstringrequired- Agent id.
evalIdstringrequired- Suite id.
runIdstringrequired- Run id.
Responses
Deleted.
curl -X DELETE "https://voice.evryhealth.com/api/v1/agents/{agentId}/evals/{evalId}/runs/{runId}" \-H "Authorization: Bearer YOUR_API_KEY"
{"data": {"deleted": true},"meta": {"requestId": "req_…"}}
Run results#
One result per scenario per iteration: the transcript of the call that was placed, what it scored on each criterion, and why. A score nobody can audit is a score nobody should act on.
/api/v1/agents/{agentId}/evals/{evalId}/runs/{runId}/resultsPath parameters
agentIdstringrequired- Agent id.
evalIdstringrequired- Suite id.
runIdstringrequired- Run id.
Responses
The results.
curl -X GET "https://voice.evryhealth.com/api/v1/agents/{agentId}/evals/{evalId}/runs/{runId}/results" \-H "Authorization: Bearer YOUR_API_KEY"
{"data": {"results": [{"id": "aa10…","evalScenarioId": "31fa…","iterationNumber": 1,"status": "completed","overallScore": 8.5,"durationSeconds": 96,"scores": [{"criterionKey": "accuracy","criterionName": "Accuracy","score": 9,"maxScore": 10,"weight": 0.4,"reasoning": "Gave the correct pickup window and did not invent a date.","evidence": "I can't give you a date yet, but the pharmacy will text you."}],"scenario": {"id": "31fa…","name": "Prescription not ready","goal": "Say the prescription is not ready and refuse to give a date.","category": "edge_case"}}]},"meta": {"requestId": "req_…"}}