> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://docs.voicebip.com/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.voicebip.com/_mcp/server.

# Verification Call-back (Voice OTP)

> Trigger an outbound call that reads a one-time code (or confirms an action). The "call me to verify" pattern — reliable where SMS delivery is flaky.

**What you build:** an API call that places an outbound call to a user and speaks a one-time code, then tells you whether they picked up. Handy for fintech onboarding and step-up auth in Nigeria, where SMS OTPs are sometimes delayed or dropped by carriers.

```
Your app stores code 4821 for +234803… 
   │
   ▼
POST /v1/calls { agent_id, from_number, to_number }
   │
   ▼
Call connects → agent fetches the code (tool/BYOM) → speaks "4 8 2 1"
   │
   ▼
call.completed webhook → status: completed → mark verification delivered
```

**Who it's for:** developers adding voice as a fallback (or primary) OTP channel, or confirming a sensitive action ("we're calling to confirm your ₦50,000 transfer").

> **Note**
>
> **Important: the call needs an agent, and the code is fetched at call time.** `POST /v1/calls` takes `{agent_id, from_number, to_number}` — there is **no per-call "greeting" or "metadata" field** to inject a dynamic code. So the code can't be baked into the request. Instead, the agent looks the code up when the call connects — via a [tool call](/use-cases/tool-calling) to your backend, or a [BYOM](/use-cases/byom-custom-llm) agent that returns the code as its first turn. Both key off the callee's number (`to_number`) or the `call_id`.

## Prerequisites

* A `from_number` on your workspace (`GET /v1/numbers`).
* A verification agent (below). You own the code lifecycle (generation, TTL, single-use) — Voicebip places the call and speaks what the agent returns.
* A webhook receiver for `call.completed`.

## Step 1 — Create the verification agent

The cleanest pattern is a **tool-calling** agent with one tool, `get_verification_code`, that your backend answers with the code for the given number. The agent's job is to greet, call the tool, and read the digits.

```json
{
  "display_name": "Acme Verification",
  "ai_provider": "hosted",
  "tts_voice": "<voice_id>",
  "webhook_url": "https://you.example.com/verify-tool",
  "greeting_text": "Hello, this is Acme with your verification code.",
  "system_prompt": "Immediately call get_verification_code. Read the returned digits one at a time, twice, then say goodbye and call hang_up. Never say anything else.",
  "tool_definitions": "[{\"type\":\"function\",\"function\":{\"name\":\"get_verification_code\",\"description\":\"Return the active verification code for the callee.\",\"parameters\":{\"type\":\"object\",\"properties\":{\"to_number\":{\"type\":\"string\"}},\"required\":[\"to_number\"]}}}]"
}
```

Your `webhook_url` receives the `tool.invocation` (see [Tool-calling](/use-cases/tool-calling)) with `tool.arguments.to_number`, looks up the code for that number, and returns e.g. `{ "code": "4, 8, 2, 1" }` (spacing the digits helps TTS read them individually).

## Step 2 — Place the call

Store the code first, then place the call. The request is just the three required fields (plus an optional per-call `webhook_url`):

```bash
curl -X POST https://api.voicebip.com/v1/calls \
  -H "Authorization: Bearer pk_xxx" -H "Content-Type: application/json" \
  -d '{
    "agent_id": "agt_verify_…",
    "from_number": "+2341002223344",
    "to_number": "+2348031234567"
  }'
```

```json
{
  "call_id": "call_5xk…",
  "agent_id": "agt_verify_…",
  "from_number": "+2341002223344",
  "to_number": "+2348031234567",
  "direction": "outbound",
  "status": "queued"
}
```

`status` is `initiated` when dialed synchronously, or `queued` when accepted onto the carrier-safe paced lane (dials when the carrier has headroom). Either way you learn the outcome from the `call.*` webhooks. **Correlate by `call_id` (returned here) or `to_number`** — there's no free-form metadata field to round-trip.

## Step 3 — Learn the outcome

`call.completed` tells you whether it connected. Treat the code as delivered only when the call actually completed and the caller was human:

```json
{
  "event_type": "call.completed",
  "payload": {
    "call_id": "call_5xk…",
    "status": "completed",
    "direction": "outbound",
    "duration_seconds": 11,
    "ai_mode": "hosted"
  }
}
```

The `Call` object's `status` distinguishes the failure modes: `completed`, `no_answer`, `busy`, `failed`. Poll `GET /v1/calls/{call_id}` if you missed the webhook.

| `status`             | Meaning                   | Your action                               |
| -------------------- | ------------------------- | ----------------------------------------- |
| `completed`          | Connected, message played | Mark OTP delivered; start the entry timer |
| `no_answer` / `busy` | Didn't connect            | Offer retry or fall back to SMS           |
| `failed`             | Carrier/routing error     | Retry; check number validity              |

## Step 4 (optional) — Confirm-an-action variant

To confirm rather than inform (*"press 1 to approve this transfer"*), give the agent a `confirm_action` tool and instruct it to call it on a spoken "yes"/"one". You can also drive DTMF programmatically with `POST /v1/calls/{call_id}/dtmf`. See [Tool-calling](/use-cases/tool-calling) for the handler.

## Test it (sandbox)

With `pk_test_`, no phone rings, but the lifecycle webhooks fire so you can exercise your verification flow end-to-end:

```bash
curl -X POST https://api.voicebip.com/v1/webhooks/test \
  -H "Authorization: Bearer pk_test_xxx" -H "Content-Type: application/json" \
  -d '{ "webhook_url": "https://you.example.com/webhooks", "event_type": "call.completed" }'
```

## Security

Treat the code like any OTP — short TTL (e.g. 5 min), single use, rate-limited per user, never logged. Voicebip redacts message content in its own logs; the code your tool returns is yours to protect.

## Troubleshooting

| Symptom                     | Likely cause                                                                          |
| --------------------------- | ------------------------------------------------------------------------------------- |
| `400` on `POST /v1/calls`   | Missing a required field — `agent_id`, `from_number`, `to_number` are all required.   |
| Code read too fast to catch | Space the digits (`"4, 8, 2, 1"`) and instruct the agent to repeat before hanging up. |
| Can't match call to user    | Correlate on `call_id` or `to_number` — there's no metadata passthrough.              |
| `402 WORKSPACE_BLOCKED`     | Balance hit zero (fail-closed); top up.                                               |

## Next steps

* Add SMS as an automatic fallback when the call doesn't connect → [Multi-channel cascade](/use-cases/multi-channel-cascade).
* Build the code-lookup tool handler → [Tool-calling](/use-cases/tool-calling).
* Run verification calls at list scale → [Campaigns deep-dive](/use-cases/campaigns-deep-dive).