Quickstart: your first report in 5 minutes
CallQA calls your voice agent like a real customer, then grades the call. Two ways to start:
let your coding agent do it (recommended), or do it by hand with curl.
Your first organization comes with 10 free test calls to try CallQA, no card needed: enough for this quickstart. See Free trial.
Set up with your coding agent (recommended)
Claude Code, Cursor, Codex or VS Code connect to the CallQA MCP server. Your agent then writes the test scenarios from your voice agent's code, verifies the phone number with you, runs the calls and fixes what fails.
- Create an API key. Open Developer > MCP server in the app (an admin), click Create key. That page then shows the commands below with your key already in them: copy from there and skip step 2.
- Or paste your key (it starts with
cqa_) in place ofPASTE_YOUR_CALLQA_KEY_HEREin the command below. Leave the wordBearerin front of it. - Install the server in your tool (one tab each), then paste the prompt that follows in your agent, opened in your voice agent's repository.
Run in a terminal (once, for all your projects), then start Claude Code in your voice agent's repository (restart it if it was open):
claude mcp add --transport http --scope user callqa https://app.callqa.dev/mcp \
--header "Authorization: Bearer PASTE_YOUR_CALLQA_KEY_HERE"
Put this in ~/.cursor/mcp.json (your user settings, not the repository; if the file
exists, add callqa to its mcpServers):
{
"mcpServers": {
"callqa": {
"url": "https://app.callqa.dev/mcp",
"headers": {
"Authorization": "Bearer PASTE_YOUR_CALLQA_KEY_HERE"
}
}
}
}
Add this to ~/.codex/config.toml:
[mcp_servers.callqa]
url = "https://app.callqa.dev/mcp"
http_headers = { "Authorization" = "Bearer PASTE_YOUR_CALLQA_KEY_HERE" }
Run in a terminal (adds it to your VS Code user profile), then use Copilot Chat in Agent mode:
code --add-mcp '{"name":"callqa","type":"http","url":"https://app.callqa.dev/mcp","headers":{"Authorization":"Bearer PASTE_YOUR_CALLQA_KEY_HERE"}}'
Any MCP client with Streamable HTTP and a custom header. Clients that only start local
(stdio) servers can use the mcp-remote bridge:
URL: https://app.callqa.dev/mcp
Header: Authorization: Bearer PASTE_YOUR_CALLQA_KEY_HERE
npx mcp-remote https://app.callqa.dev/mcp --header "Authorization: Bearer PASTE_YOUR_CALLQA_KEY_HERE"
The prompt to paste in your agent:
Set up CallQA to test my voice agent (the one in this repository). The CallQA MCP server is
installed as "callqa": use its tools and follow its set-up-callqa steps.
1. Call get_setup_status first: it says what is done and which tool does each next step.
2. Environments (staging, production), then my agent's phone number and its verification.
3. Write 2 to 4 test scenarios from this repository (the agent's prompt, tools and flows),
validate them, save them.
4. One first run: read the report, fix my agent (or the test) where it fails.
5. Schedule the tests every weekday morning and add the CI files.
Rules: before a run, tell me how many real calls it places. Stop and ask me for what only a
person can do (signing the number attestation, putting the verification phrase live,
billing). Never write an API key in a file of this repository. If no callqa tool is
available, tell me to install the MCP server first.
Your agent stops twice for you: to sign the attestation that you control the number,
and to put the verification phrase live if your agent's prompt is not in the repository.
In Claude Code, the same steps are also the slash command /mcp__callqa__set-up-callqa.
Details: MCP server.
Or by hand, with curl
You need curl, jq, and a phone number you own or are allowed to test.
1. Your API key
In the app, an admin opens Developer > API keys
and clicks + API key, with the scopes read, write and run. The key is shown
once and looks like cqa_1a2b3c4d_Zr0.... Put your key in place of
PASTE_YOUR_CALLQA_KEY_HERE:
export CALLQA_API_KEY="PASTE_YOUR_CALLQA_KEY_HERE"
export CALLQA="https://app.callqa.dev/api/v1"
curl -sS -H "Authorization: Bearer $CALLQA_API_KEY" "$CALLQA/me" | jq '{org_name, scopes, plan: .plan.code}'
{ "org_name": "Acme", "scopes": ["read", "run", "write"], "plan": "growth" }
A 401 means the key is wrong or still the placeholder.
2. Import a scenario
Save this as reschedule.yaml, with your phone number (E.164 format):
name: "Reschedule a dentist appointment"
phone: "+14155550107"
persona: "Patient who needs to move Thursday's cleaning to next week"
goal: "Move the appointment to any morning next week"
must_include:
- "confirm the new date and time"
max_latency_ms: 2000
variants:
- title: "Polite patient"
- title: "Patient in a hurry"
persona:
description: "Patient calling between two meetings, impatient, speaks fast"
Import it (the YAML is the raw request body):
curl -sS -X POST "$CALLQA/scenarios/import" \
-H "Authorization: Bearer $CALLQA_API_KEY" \
-H "Content-Type: application/yaml" \
--data-binary @reschedule.yaml | tee scenario.json | jq '{id, name, target_id, variants}'
export SCENARIO=$(jq -r .id scenario.json)
export TARGET=$(jq -r .target_id scenario.json)
201 means created; importing the same name again updates it (200). An invalid document
answers 422 with the fields at fault, for example
{"loc": "target.phone", "msg": "Value error, phone must be E.164, e.g. +15550123456"}.
3. Verify the number (one time)
CallQA only calls numbers you control. A new number is unverified: verify it once, in the
app (Monitoring > Phone numbers, open the number, Verify ownership) or with the API
(scopes run and write). You get a 6-digit code, add one sentence with it to the line's
greeting (or agent prompt), and we call once to hear it:
# 1. a code, shown once (the attestation is signed the first time)
curl -sS -X POST "$CALLQA/targets/$TARGET/verification/start" \
-H "Authorization: Bearer $CALLQA_API_KEY" \
-H "Content-Type: application/json" \
-d '{"attestation": {"accept": true, "signer_name": "Jane Doe, CTO"}}' \
| jq '{phrase, code_expires_at}'
# 2. add the phrase ("Verification code 4 8 2 1 9 3.") to the greeting, then:
curl -sS -X POST "$CALLQA/targets/$TARGET/verification/call" \
-H "Authorization: Bearer $CALLQA_API_KEY"
# 3. about 30 seconds later
curl -sS "$CALLQA/targets/$TARGET/verification" \
-H "Authorization: Bearer $CALLQA_API_KEY" | jq '{status, last_attempt}'
status becomes verified (valid 180 days). The attestation says your organization owns the
number or is authorized to test it; it is kept as proof with the signer, IP and time. If your line only says the code after a
menu, add "dtmf_gate": "1w2" (the keys to press first) to the start request. Details:
Number verification.
4. Run the scenario
curl -sS -X POST "$CALLQA/scenarios/$SCENARIO/runs" \
-H "Authorization: Bearer $CALLQA_API_KEY" \
-H "Content-Type: application/json" \
-d '{"tags": ["quickstart"]}' \
| tee run.json | jq '{id, number, status, total, url}'
export RUN=$(jq -r .id run.json)
One call per enabled variant (here 2), in parallel within your plan's concurrency limit; a
call takes 1 to 5 minutes. Add "environment": "staging" and "version": "v2.3.0" to the
body to test a given deployment (see Environments). While a call is in
progress, its page in the app has a Listen live button.
5. Wait for the result
while :; do
curl -sS -H "Authorization: Bearer $CALLQA_API_KEY" "$CALLQA/runs/$RUN" > run.json
status=$(jq -r .status run.json)
echo "status: $status"
[ "$status" = "completed" ] || [ "$status" = "canceled" ] && break
sleep 15
done
jq '{verdict, score, passed, failed, warned, errored, blocked, cost_cents, url}' run.json
{
"verdict": "pass", "score": 91, "passed": 2, "failed": 0, "warned": 0,
"errored": 0, "blocked": 0, "cost_cents": 0,
"url": "https://app.callqa.dev/o/acme/runs/5c1e.../"
}
cost_cents stays 0 while calls fit in your monthly quota. The run is also in the app
under Monitoring > Runs and calls.
6. Read the report
curl -sS -H "Authorization: Bearer $CALLQA_API_KEY" "$CALLQA/calls?run_id=$RUN" \
| jq '.items[] | {id, title, status, verdict, score, latency_p95_ms}'
CALL=$(curl -sS -H "Authorization: Bearer $CALLQA_API_KEY" "$CALLQA/calls?run_id=$RUN" | jq -r '.items[0].id')
curl -sS -H "Authorization: Bearer $CALLQA_API_KEY" "$CALLQA/calls/$CALL" \
| jq '{verdict, score, verdicts: [.result.verdicts[] | {type, passed, evidence}], transcript: [.transcript[] | "\(.speaker): \(.text)"]}'
Each verdict quotes the transcript as evidence when there is one (evidence is null for
checks such as answered or max_silence; latency_p95_ms is null when no turn latency
was measured, as on a keypad-only menu). The full event trace is at GET /calls/{id}/events.
To try any endpoint from your browser, use the API explorer.
If a call did not happen
A call that could not be placed ends with a blocked_* status and a block_reason:
status |
block_reason |
Fix |
|---|---|---|
blocked_policy |
number_not_verified |
Verify the number (step 3). |
blocked_policy |
sandbox_no_batch, sandbox_daily_cap |
The number is not verified yet: only a few manual sandbox calls. Verify it. |
blocked_insufficient_credit |
insufficient_credit |
Quota used up: top up the prepaid wallet. See Billing. |
blocked_policy |
country_not_in_plan |
Your plan allows fewer countries. |
The full list is in Errors.
Next steps
- Let your coding agent run the tests, read the failures and fix your agent: MCP server.
- Gate your deployments on the score: CI/CD integration.
- Get notified instead of polling: Webhooks.
- Write richer scenarios (IVR steps, typed assertions, test data): Writing scenarios and the grammar reference.