Skip to navigation

Generate a simulation suite with the API

Draft requirements for an assistant, generate a simulation suite from them, edit it, and run it

The Simulations onboarding API builds a simulation suite for an assistant from its prompt, tools, and handoffs. It runs the same flow as Composer. Call these three endpoints from a script, a CI job, or a coding agent:

StepEndpointTakes up to
Draft requirementsPOST /eval/simulation/onboarding/requirements2 minutes
Generate a suitePOST /eval/simulation/onboarding/suite3 minutes
Edit the suitePATCH /eval/simulation/onboarding/suite/{suiteId}2 minutes

You then run the suite with the existing POST /eval/simulation/run endpoint.

Beta. During the beta, only organizations with beta access can call these endpoints with a private API key. Other organizations get a 404. Contact support to ask about access. Composer and the Dashboard don’t need this access.

Prerequisites

  • An assistant to test. Copy its ID from its page in the Dashboard.
  • A private API key in VAPI_API_KEY. The key needs permission to create and update simulation suites.
  • An HTTP client with a long timeout. Suite generation can take up to 3 minutes, so set a client timeout of at least 200 seconds.

Generated simulations mock every tool in the assistant’s tool list that would reach an external system. The response lists them in mockedTools. The following can still reach live systems:

  • Tools that only an assistant hook calls.
  • Webhooks you set explicitly on a scenario.
  • Requests to your server from tools or destinations that aren’t mocked.
  • Tools you add to a simulation later, or mocks you turn off.

Simulation lifecycle server messages, including the end-of-call report, are suppressed. Suppressing them doesn’t stop the requests above. See Mock tool responses.

Generate and run a suite

1

Draft requirements

Send the assistant ID. The endpoint reads the assistant’s prompt, tools, and handoffs, and drafts the behaviors the assistant must get right as numbered requirements, plus a few open questions.

curl --fail-with-body -sS -X POST "https://api.vapi.ai/eval/simulation/onboarding/requirements" \
--max-time 200 \
-H "Authorization: Bearer $VAPI_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "assistantId": "<assistant-id>" }' \
> draft.json

The response is a draft:

{
"version": 1,
"sources": ["System prompt", "Tools: bookAppointment, checkAvailability"],
"requirements": [
{ "id": "R1", "text": "Confirms the date, time, and time zone before booking." },
{ "id": "R2", "text": "Offers another slot when the requested time is unavailable." },
{
"id": "R3",
"text": "Does not book outside office hours.",
"tag": "assumption",
"note": "Office hours are not in the prompt; assumed Monday to Friday, 9am to 5pm."
}
],
"openQuestions": [
{
"id": "Q1",
"text": "What are the office hours?",
"why": "R3 depends on them."
}
]
}

Review the requirements and answer the open questions. A requirement tagged assumption is based on a guess. A requirement tagged not-testable describes behavior generated simulations can’t verify, such as latency. A requirement tagged voice-only needs a voice run with a call recording. Its checks are skipped in chat runs.

2

Revise the draft (optional)

To change the draft, call the same endpoint again. Send the draft exactly as you received it in previousDraft, and what you want changed in feedback. The two fields must be sent together.

jq -n --slurpfile draft draft.json '{
assistantId: "<assistant-id>",
previousDraft: $draft[0],
feedback: "Office hours are Monday to Friday, 8am to 6pm. Drop R2: we never offer another slot."
}' > revise.json
curl --fail-with-body -sS -X POST "https://api.vapi.ai/eval/simulation/onboarding/requirements" \
--max-time 200 \
-H "Authorization: Bearer $VAPI_API_KEY" \
-H "Content-Type: application/json" \
-d @revise.json \
> draft.json

The new draft has a higher version and a changeNote describing what changed. Repeat until you agree with the requirements. Each revision counts toward the daily limit.

3

Generate the suite

Send the agreed requirements and an idempotencyKey. Use a new key for each agreed draft. When you retry that draft, send the same key again. A retry with the same key never generates a second suite. See Idempotency and retries.

jq -n --slurpfile draft draft.json '{
assistantId: "<assistant-id>",
idempotencyKey: "booking-agent-draft-2",
requirements: $draft[0].requirements
}' > suite.json
curl --fail-with-body -sS -X POST "https://api.vapi.ai/eval/simulation/onboarding/suite" \
--max-time 200 \
-H "Authorization: Bearer $VAPI_API_KEY" \
-H "Content-Type: application/json" \
-d @suite.json

The endpoint plans scenarios that cover the requirements, writes each scenario and its success criteria, mocks the tools, then saves the suite and assigns it to the assistant. The response describes the saved suite:

{
"suite": { "id": "<suite-id>", "name": "Riley: core behaviors" },
"assistantId": "<assistant-id>",
"simulations": [
{
"id": "<simulation-id>",
"scenarioId": "<scenario-id>",
"name": "Books a slot inside office hours",
"personalityName": "Skeptical Sam",
"successCriteria": ["Did the assistant confirm the date, time, and time zone before booking?"],
"requirementIds": ["R1", "R3"],
"mockedTools": ["bookAppointment", "checkAvailability"]
}
],
"coverage": {
"covered": 2,
"total": 3,
"gaps": [
{ "requirementId": "R2", "reason": "budget", "detail": "It did not fit in this generation." }
],
"retryable": 1
},
"requirements": [{ "id": "R1", "text": "Confirms the date, time, and time zone before booking." }],
"docs": { "bestPractices": "https://docs.vapi.ai/...", "toolMocks": "https://docs.vapi.ai/..." },
"reused": false
}

Check coverage.gaps for requirements no simulation covers, and toolGaps for tool paths the simulations don’t exercise. See Coverage gaps and Tool gaps.

4

Edit the suite (optional)

Edit the suite with a new patchKey for each edit. In one request, you can:

  • Remove simulations with removeSimulationIds. They are unlinked from the suite, not deleted.
  • Rewrite up to 10 simulations with edits, each an instruction in your own words. Put every change for a simulation in one instruction.
  • Set retryFailed: true to regenerate scenarios that failed or didn’t fit. The coverage.retryable field is the number of scenarios a retry would regenerate.
curl --fail-with-body -sS -X PATCH "https://api.vapi.ai/eval/simulation/onboarding/suite/<suite-id>" \
--max-time 200 \
-H "Authorization: Bearer $VAPI_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"patchKey": "booking-agent-edit-1",
"edits": [
{
"simulationId": "<simulation-id>",
"instruction": "Make the caller ask for 7pm first, then accept 5pm."
}
],
"retryFailed": true
}'

The response is the updated suite. If an edit could not be applied, for example because the rewrite did not generate in time, its simulation ID is listed in failedEditSimulationIds. Retry those with a new patchKey.

5

Run the suite

Run the suite against the assistant with POST /eval/simulation/run. Send an Idempotency-Key header so a retried request returns the run the first request created instead of starting another.

curl --fail-with-body -sS -X POST "https://api.vapi.ai/eval/simulation/run" \
-H "Authorization: Bearer $VAPI_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: booking-agent-run-1" \
-d '{
"simulations": [{ "type": "simulationSuite", "simulationSuiteId": "<suite-id>" }],
"target": { "type": "assistant", "assistantId": "<assistant-id>" },
"transport": { "provider": "vapi.webchat" },
"iterations": 5
}'

Poll GET /eval/simulation/run/<run-id> until status is ended, as in the Simulations quickstart. Run five or more iterations before you trust a pass rate, and read the failed transcripts. See Test outcomes with Simulations.

Request fields

Draft requirements

FieldTypeDescription
assistantIdstring (UUID)Required. The assistant to draft requirements for.
previousDraftobjectThe draft being revised, exactly as this endpoint returned it. Send it with feedback.
feedbackstringWhat you want changed in previousDraft, up to 8,000 characters. Send it with previousDraft.

Generate a suite

FieldTypeDescription
assistantIdstring (UUID)Required. The assistant the suite tests and is assigned to.
idempotencyKeystringRequired. 1 to 200 characters. One key per agreed draft.
requirementsarrayRequired. 1 to 50 requirements from the draft, each with a unique id. Pass them as the draft returned them.

Edit a suite

The suiteId in the path must be the ID of a suite generated by POST /eval/simulation/onboarding/suite (or by Composer) in your organization.

FieldTypeDescription
patchKeystringRequired. 1 to 200 characters. A new key for each edit.
removeSimulationIdsarray of UUIDsSimulations to unlink from the suite, up to 100.
editsarrayUp to 10 { simulationId, instruction } objects, at most one per simulation. instruction is up to 8,000 characters.
retryFailedbooleanRegenerate the scenarios that failed or did not fit when the suite was generated.

Response fields

Requirements draft

FieldDescription
versionDraft version, 1 for a first draft.
changeNoteWhat changed since the previous draft.
sourcesWhat the draft was based on, such as the prompt and tools.
requirementsUp to 50 requirements. Each has an id (such as R3), text (one observable behavior), and optionally a tag (assumption, not-testable, or voice-only) and a note explaining the tag.
openQuestionsUp to 3 questions to answer before you agree. Each has an id (such as Q1), text, and why (which requirement the answer changes, and how).

Suite

Generating and editing a suite both return this object.

FieldDescription
suiteThe suite’s id and name. Pass the id to edit or run the suite.
assistantIdThe assistant the suite is assigned to.
simulationsThe suite’s simulations, in order. Each has an id, scenarioId, name, optional personalityName, successCriteria (each as the yes/no question it is judged by), requirementIds (the requirements it covers), and mockedTools.
coveragecovered and total requirements, gaps (requirements no simulation covers, each with a requirementId, reason, and optional detail), and retryable (planned scenarios an edit with retryFailed would regenerate).
requirementsThe agreed requirements the suite was generated from.
docsbestPractices and toolMocks documentation links.
reusedtrue when the request matched one already applied (the same idempotencyKey or patchKey) and nothing changed.
toolGapsTool paths the simulations don’t exercise as configured. Each has a toolName, an assistantName when the tool belongs to a handoff destination, a reason, and optionally the destinationNames the simulations don’t reach. Omitted when there are none.
failedEditSimulationIdsEdit responses only. Simulations an edit asked to rewrite that were not rewritten.

Coverage gaps

A requirement with no simulation covering it appears in coverage.gaps with one of these reasons:

ReasonWhat it meansWhat to do
not-testableThe requirement can’t be verified by a generated simulation with the assistant’s current configuration, such as latency or an audio check without a recording.Read the requirement’s note. Change the configuration if possible, or test the behavior outside generated simulations.
unmappedNo planned scenario exercises the requirement, or its simulation was removed from the suite.Reword the requirement as an observable behavior and generate a new suite, or add a simulation by hand.
failedIts scenario could not be generated or saved in time.Edit the suite with retryFailed: true.
budgetIt didn’t fit in one generation’s time budget.Edit the suite with retryFailed: true.

Tool gaps

The toolGaps array lists tool paths the simulations don’t exercise as configured. Most are mocked handoffs or transfers. Running them for real could reach your server, a phone number, or a destination that couldn’t be checked safely. A mocked handoff or transfer answers as if it succeeded, so the conversation continues, but the destination isn’t tested. New reasons may be added.

ReasonWhat it means
dynamic-handoffYour server chooses the handoff destination at call time, so the handoff is mocked.
destination-not-plainThe destination assistant uses MCP tools, a pinned assistant version, or other configuration that generation doesn’t resolve, so the handoff into it is mocked.
destination-unresolvedThe destination assistant couldn’t be loaded, so the handoff into it is mocked.
destination-truncatedThe destination wasn’t checked in time, either because the assistant has too many handoff destinations or because the destination loaded too slowly. The handoff into it is mocked.
transfer-externalA transferCall can leave Vapi, to a phone number, a SIP address, or a destination your server chooses, so it is mocked.
destination-name-unscopedA handoff names a destination assistant outside a squad, which can’t be resolved at runtime, so that destination is never reached. The handoff isn’t mocked.
native-tool-shadowedA built-in tool, such as endCall or transferCall, has the same name as a mocked tool, so the mock answers it too.

To test a mocked path, run a separate simulation against the destination assistant, or replace the mock with a sandbox integration. See Mock tool responses.

Idempotency and retries

Suite generation. Send one idempotencyKey per agreed draft, and retry with the same key:

  • On a timeout or a 502, 504, or 524, retry with the same key. You get the saved suite, a 409 while it is still generating, or a fresh generation if the first one never saved.
  • On a 409 with SIMULATION_ONBOARDING_GENERATION_IN_PROGRESS, the first request is still generating. Wait Retry-After seconds (15) and retry with the same key until you get the suite.
  • On a 409 with SIMULATION_ONBOARDING_IDEMPOTENCY_KEY_REUSED, the key was already used for a different assistant or different requirements. Don’t retry with it; send a new key.

A retry that returns a saved suite sets reused to true and doesn’t count toward your limits. To generate a new suite from a revised draft, use a new key.

Edits. Send a new patchKey for each edit. To retry an edit after a timeout or a 5xx, send the same key again. If the suite already applied that key, nothing changes. You get the current suite, with failedEditSimulationIds listing what that edit couldn’t rewrite.

A suite remembers only the last 50 patch keys it applied. If you reuse one of those keys for a different edit, the new edit is ignored and you get the current suite. Once a key drops out of the last 50, a request with it can apply again. Never reuse a key for a different edit.

Drafts. Drafting has no key, so each retry requests a new draft. A request counts toward your daily limit once generation starts, even if it later fails. Requests refused before generation starts, such as a 404 or a 429, don’t count.

Limits

Each organization has a free daily allowance, shared by the API, the Dashboard, and Composer. Limits reset at 00:00 UTC.

OperationDaily limit per organization
Requirement drafts and revisions60
Suite generations20
Suite edits60

An organization can also have at most 2 suite generations running at once.

These don’t count toward the daily limits:

  • A retry that returns a saved suite, or a replayed patchKey.
  • An edit that only removes simulations, or retryFailed when nothing is left to retry.
  • A request refused before generation starts, such as a 404 or a 422.

A request counts once generation starts, even if it later fails with a 502 or times out.

The same limits apply to every plan, including Enterprise. No request field raises them.

When you reach a limit, the API returns 429:

CodeMeaningWhat to do
SIMULATION_ONBOARDING_QUOTA_EXCEEDEDThe organization used its daily allowance for this operation.Don’t retry. The limit resets at 00:00 UTC; Retry-After is the number of seconds until then.
SIMULATION_ONBOARDING_CONCURRENCY_LIMITEDThe organization already has 2 suites generating.Wait Retry-After seconds (30), then retry.

Errors

The 409, 429, and quota-unavailable 503 responses carry a JSON body and a Retry-After header when waiting helps:

{
"code": "SIMULATION_ONBOARDING_QUOTA_EXCEEDED",
"message": "This org has used its 20 free suite generations for today. The limit resets at 00:00 UTC.",
"retryAfterSeconds": 41230,
"limit": 20
}
FieldDescription
codeWhich refusal this is. Branch on it.
messageWhat happened, written for the user.
retryAfterSecondsSeconds to wait before retrying, the same as Retry-After. For a daily limit, the seconds until 00:00 UTC.
limitThe daily limit that was reached, on a daily-limit refusal only.
StatusWhenWhat to do
400The body is invalid.Fix the request.
400The request sends only one of previousDraft and feedback.Send both fields together.
400The generated or edited suite uses something your organization’s data-region policy doesn’t allow, so it isn’t saved.Use a configuration allowed in your data region.
401The API key is missing or invalid.Use a valid private API key.
403The key can’t create or edit simulation suites.Use a key with that permission.
404The assistant wasn’t found; the suite isn’t a generated suite in your organization; or your organization doesn’t have API access yet.Check the ID, or contact support to enable access.
409SIMULATION_ONBOARDING_GENERATION_IN_PROGRESS or SIMULATION_ONBOARDING_IDEMPOTENCY_KEY_REUSED. Suite generation only.See Idempotency and retries.
422None of the requirements can be checked by generated simulations. Applies to generation only.Add a testable requirement.
422An MCP server’s tools couldn’t be listed, so simulations would call them for real instead of a mock. Applies to generation and edits.Check that the MCP server is reachable, then retry.
429A daily limit or the concurrent-generation limit was reached.See Limits.
500The assistant, its tools, or the suite couldn’t be read.Retry. For generation and edits, retry with the same key.
502The model failed to generate a usable result.Retry. For generation and edits, retry with the same key.
503Simulation generation is paused, or no AI tester personalities are configured.Retry later.
503SIMULATION_ONBOARDING_QUOTA_UNAVAILABLE: the quota couldn’t be enforced for a private-key request.Wait Retry-After seconds (15), then retry. For generation and edits, keep the same key.
504The request took too long.Retry. For generation and edits, retry with the same key. A generation may still save.

Next steps