Back to app

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.

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.

  1. 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.
  2. Or paste your key (it starts with cqa_) in place of PASTE_YOUR_CALLQA_KEY_HERE in the command below. Leave the word Bearer in front of it.
  3. 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