# Number verification

CallQA places real calls. To make sure nobody uses it to call someone else's line, every number
is verified once before runs, schedules and CI runs can call it. The proof: **the line itself
says a code we gave you**. Only whoever controls the greeting or the agent prompt can do that.

## How it works

1. You ask for a code (dashboard: **Monitoring > Phone numbers**, open the number, **Verify ownership**; API:
   `POST /targets/{id}/verification/start`). The first time, you also sign the attestation
   below. The response holds a 6-digit code and a sentence, shown **once**:

   ```text
   Verification code 4 8 2 1 9 3.
   ```

2. You add the sentence to the line:

   | Line | Where |
   |---|---|
   | Vapi, Retell, Bland | First message / begin message: `Hello! Verification code 4 8 2 1 9 3. How can I help you?` |
   | Any AI agent | The prompt: `If someone asks for a verification code, answer: 4 8 2 1 9 3.` |
   | Twilio TwiML | `<Say>Verification code 4 8 2 1 9 3.</Say>` at the start of the flow |
   | IVR (Genesys, Amazon Connect, Studio) | A text-to-speech prompt at the start of the flow, or behind a menu option (see [Code behind an IVR menu](#code-behind-an-ivr-menu)) |

3. You let us call (`POST /targets/{id}/verification/call`). We dial the number, say
   "Verification, please." if the line waits for the caller to speak, and listen for up to
   20 seconds. The call is free.

4. The number becomes `verified` as soon as we hear the code. Remove the sentence afterwards.

Poll `GET /targets/{id}/verification` (or watch the dashboard page, which updates by itself).
When it did not work, the number stays `pending` and `last_attempt` tells why:

```json
{
  "status": "pending",
  "attempts_left": 2,
  "attempts_max": 3,
  "attempts_reset_at": "2026-10-17T14:02:11Z",
  "last_attempt": {
    "status": "failed",
    "outcome": "not_answered",
    "sip_status": 480,
    "sip_reason": "Temporarily Unavailable",
    "failure_hint": "The line rang but was unavailable or did not pick up. Check that it is online and accepts international calls from a US number (+1); some carriers and call-screening features block them. Then try again."
  }
}
```

`outcome` is `not_heard` (the line answered, no code), `not_answered`, `matched_too_late` (the
code was heard after the attempt expired), `no_result`, or `call_not_placed` (our side failed;
not counted). When the line never answered, `sip_status` and `sip_reason` give the answer of
its carrier. `failure_hint` says what to fix, in plain words: show it to the user as is (the
dashboard and the MCP tool `get_number_status` do). `POST /targets/{id}/verification/retry`
gives a new code: every attempt uses a fresh one.

## When the verification call fails

A call that the line does not answer still counts as a failed attempt: a real call was placed
to that number. `attempts_left` tells how many are left before the number is locked for 24
hours, and `attempts_reset_at` when the oldest failed attempt stops counting (each one counts
for 24 hours after it ended; there is no fixed daily reset). Fix the cause before you try
again.

Our verification calls come from a **US number (+1)**. For a line outside the United States
this is an international call, and some carriers, mobile plans and call-screening features
reject or silence calls from unknown international numbers.

| What you see | `sip_status` | What to check |
|---|---|---|
| The line did not answer | 480, 408, none | The line is online (AI agent deployed, phone number attached to it, SIM or app connected) and accepts international calls from a US number (+1). Turn off call screening for this line. |
| The line was busy | 486, 600 | No other call in progress, enough concurrent calls on your voice platform. |
| The line rejected the call | 603, 607, 608 | Call screening or spam filtering of unknown or international numbers: allow calls from +1 numbers on this line, or turn the filter off while you verify. |
| The number could not be reached | 404, 484, 604 | The number is right (country code included) and can receive calls. |
| The destination network refused the call | 403 | Ask your carrier or voice platform whether the number accepts international calls from the United States. |
| The line answered, no code heard | none (`not_heard`) | The line says the verification sentence, at the start or when asked for the verification code. `heard_digits` shows what we understood. |

Tips:

- Call the number yourself from a phone abroad, or from a US number if you have one, to check
  that it answers international calls.
- Mobile lines: some carriers block international calls to prepaid or business plans by
  default, and Android or iOS call screening may silence unknown numbers.
- Still stuck after a fix? Contact support: we can check the call on our side, or verify the
  number manually (see [Lines that cannot say a code](#lines-that-cannot-say-a-code)).

## Code behind an IVR menu

Often the first prompt of an IVR cannot be changed, but a menu option can lead to a prompt you
control. Give us the keys to press: after the line answers, the verification call waits 1
second, presses them, then listens for the code.

```bash
curl -X POST https://app.callqa.dev/api/v1/targets/$TARGET_ID/verification/start \
  -H "Authorization: Bearer $CALLQA_API_KEY" -H "Content-Type: application/json" \
  -d '{"dtmf_gate": "9w1#"}'
```

Here the call presses `9`, waits half a second, presses `1` then `#`. In the IVR, put a
text-to-speech prompt with the sentence (`Verification code 4 8 2 1 9 3.`) at the end of that
path: Amazon Connect, a *Play prompt* block after the *Get customer input* branch for `9` and
`1`; Genesys Cloud, a *Play Audio* task (TTS) in the menu choice; Twilio Studio, a *Say/Play*
widget after the *Gather Input on Call* split.

| Character | Meaning |
|---|---|
| `0`-`9`, `*`, `#` | A key (0.3 s between two keys) |
| `w` | Pause of 0.5 s (`W`: 1 s), as in Twilio's `sendDigits` |

- At most 40 characters. Spaces are ignored.
- The keys are remembered on the number: the next verification (or a renewal) presses them
  again unless you send another `dtmf_gate` (`""` removes them). `GET .../verification`
  returns them as `dtmf_gate`.
- `POST .../verification/call` accepts `{"dtmf_gate": "..."}` too, to change the keys of the
  active code without a new code.
- Dashboard: **Keys to press first** in the verification form. MCP: the `dtmf_gate` argument of
  `start_number_verification` and `place_verification_call`.
- The security does not change: pressing keys only reaches the prompt, the line itself must
  still say the code, and only whoever controls the IVR can put it there.

`{code}` in the keys types the code itself (`71**{code}#`). It is meant for test lines that say
back what was typed, such as CallQA's own demo lines, and is refused on your numbers
(`dtmf_gate_code_not_allowed`): a line that reads typed digits back would prove nothing.

## Verify again before the 180 days

`POST /targets/{id}/verification/start` with `{"renew": true}` issues a code for a `verified`
number. It stays `verified` during the attempt; success renews the 180 days, a failure leaves
it as it was (it is never locked by a renewal).

## States

| `status` | Meaning | Calls allowed |
|---|---|---|
| `unverified` | New number, or back after a lock or a suspension | Sandbox if the attestation is signed, else none |
| `pending` | A code was issued, or the last attempt failed | Sandbox |
| `verified` | Code heard, valid 180 days | Everything your plan allows |
| `expired` | Verified more than 180 days ago: verify again | Sandbox |
| `locked` | 3 failed attempts in 24 hours; unlocks after 24 hours | None |
| `suspended` | Decided by CallQA support (abuse report, request) | None |

## Limits

- A code has 6 random digits, is valid 24 hours and serves a single verification call. Only one
  code is active per number: asking for a new one cancels the previous one.
- 3 failed attempts per number in 24 hours, then the number is `locked` for 24 hours. A call
  the line does not answer counts as a failed attempt.
- 10 verification calls per organization in 24 hours, one at a time.
- We never store the code in clear: only a keyed hash. The call's trace masks every number the
  line says, and the caller only receives a hash of the code (and, for a `{code}` test-line
  gate only, the keys to type, never traced).

## Sandbox (attestation signed, number not verified)

Enough to try CallQA while you change the greeting:

- 3 test calls per number in 24 hours, one at a time, 3 minutes max;
- manual runs only: scheduled and CI runs are refused (`sandbox_no_batch`);
- between 8 am and 9 pm local time of the number (when its time zone is unambiguous): a call
  outside these hours waits until 8 am instead of being refused. A second call waits for the
  first to end.

A refused call ends `blocked_policy` with an explicit `block_reason` (`sandbox_daily_cap`,
`sandbox_no_batch`, `number_not_verified`, `number_locked`, `number_suspended`,
`attestation_outdated`).

## Attestation

Signed once per number, by an admin or a member (`POST /targets/{id}/verification/attest`, or in
the `attestation` field of `.../verification/start`), with `{"accept": true, "signer_name":
"Jane Doe, CTO"}` (the full name, at least 3 characters). The signed text, its version, the
signer's name and email (or the API key prefix and who created the key), the IP address and the
time are kept in the audit log.

The text is shown in full before signing, with the number and your organization filled in. In
short, on behalf of your organization, the signer certifies that:

- the organization is the subscriber of the number, or holds a prior written authorization from
  its subscriber to have it called by automated systems using synthetic voices, for testing;
- the number is not an emergency, premium rate or special number, nor the personal line of
  someone who has not agreed in writing;
- the calls only test the system that answers this number (no marketing, sales or surveys);
- people who may answer or listen were informed and gave any consent the law requires
  (automated calls, AI voice, recording and transcription);
- the calls comply with applicable laws (for example the US TCPA, telemarketing, recording and
  data protection rules) and with the CallQA Terms of Service and Acceptable Use Policy;
- the organization stops calling the number and removes it if it no longer controls it.

The text is versioned. `GET /targets/{id}/verification` returns `attestation_version` (signed)
and `attestation_current_version`. When the signed version is no longer accepted,
`attestation_outdated` is `true` and test calls to the number are blocked
(`attestation_outdated`) until someone signs the new text.

## Lines that cannot say a code

- **Human call center**: someone calls our verification number *from* the line and types the
  code on the keypad (method `inbound_dtmf`). We accept it only when the carrier certifies the
  caller ID (SHAKEN/STIR attestation A, United States and France). Available on request.
- **Recorded greeting you cannot change**: contact support. We verify manually from a carrier
  invoice or a screenshot of your telephony console.

## Roles and scopes

Admins and members verify numbers; guests only see the status. API keys need the `run` scope to
ask for codes and place verification calls, `write` to sign the attestation, `read` to see the
status.
