Verification Call-back (Voice OTP)

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”).

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 to your backend, or a BYOM 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.

1{
2 "display_name": "Acme Verification",
3 "ai_provider": "hosted",
4 "tts_voice": "<voice_id>",
5 "webhook_url": "https://you.example.com/verify-tool",
6 "greeting_text": "Hello, this is Acme with your verification code.",
7 "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.",
8 "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\"]}}}]"
9}

Your webhook_url receives the tool.invocation (see 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):

$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"
> }'
1{
2 "call_id": "call_5xk…",
3 "agent_id": "agt_verify_…",
4 "from_number": "+2341002223344",
5 "to_number": "+2348031234567",
6 "direction": "outbound",
7 "status": "queued"
8}

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:

1{
2 "event_type": "call.completed",
3 "payload": {
4 "call_id": "call_5xk…",
5 "status": "completed",
6 "direction": "outbound",
7 "duration_seconds": 11,
8 "ai_mode": "hosted"
9 }
10}

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.

statusMeaningYour action
completedConnected, message playedMark OTP delivered; start the entry timer
no_answer / busyDidn’t connectOffer retry or fall back to SMS
failedCarrier/routing errorRetry; 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 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:

$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

SymptomLikely cause
400 on POST /v1/callsMissing a required field — agent_id, from_number, to_number are all required.
Code read too fast to catchSpace the digits ("4, 8, 2, 1") and instruct the agent to repeat before hanging up.
Can’t match call to userCorrelate on call_id or to_number — there’s no metadata passthrough.
402 WORKSPACE_BLOCKEDBalance hit zero (fail-closed); top up.

Next steps