Send a Voice OTP in 5 minutes
This tutorial places one automated voice call that speaks a one-time passcode. You will install the SDK, send the call, and confirm the event was accepted.
Voice is opt-in per event. Unlike SMS, it is not inferred from the recipient
having a phone number — a call costs roughly three times an SMS and rings
someone, so you must ask for it by supplying overrides.voice.
Before you start
- An API key from the dashboard. A
nerve_sk_test_...key runs in Test Mode: no carrier is called and nothing is billed. - A destination phone number in E.164 format
(
+2348012345678). The number must be on thetorecipient. - Node.js 24 or later if you follow the SDK path.
1. Install the SDK
npm install @nervehq/sdk
2. Place the call
import { Nerve } from '@nervehq/sdk';
const nerve = new Nerve({ apiKey: process.env.NERVE_API_KEY! });
const { eventId } = await nerve.voice.send({
name: 'otp-login',
category: 'security',
to: { subscriberId: 'user_8f21c', phone: '+2348012345678' },
script: 'Your verification code is 4827. It expires in five minutes.',
// Optional. Only Infobip honours a named voice profile today.
voice_id: 'Ada',
language: 'en-US',
// OTP is critical, so voice is attempted first rather than ordered behind SMS.
priority: 'CRITICAL',
});
console.log(`Voice call queued as ${eventId}`);
The SDK returns as soon as the gateway has durably accepted the event, with
channel: 'voice'. The call itself is placed asynchronously.
overrides.voice makes the call eligible; it does not by itself make it
first. A NORMAL or LOW event without extraParams.voice_primary is ordered
by cost, so SMS is tried first and the call is the failover. Only CRITICAL /
HIGH priority (as above) or extraParams.voice_primary=true puts voice first.
See Voice routing.
The worker rewrites a run of four or more digits into a comma-separated
sequence so the text-to-speech engine speaks each digit: 4827 becomes
4, 8, 2, 7. You write the code the way a human reads it; the pacing is
applied for you. See the Voice reference for the exact
rule.
3. Or send it with cURL
Every SDK call is a thin wrapper over the gateway. The same call looks like this on the wire:
curl -X POST https://api.nervly.io/v1/events/trigger \
-H "Authorization: Bearer $NERVE_API_KEY" \
-H "X-Priority-Override: CRITICAL" \
-H "Content-Type: application/json" \
-d '{
"name": "otp-login",
"to": { "subscriberId": "user_8f21c", "phone": "+2348012345678" },
"category": "security",
"overrides": {
"voice": {
"script": "Your verification code is 4827. It expires in five minutes.",
"voice_id": "Ada",
"language": "en-US"
}
}
}'
{
"eventId": "evt_9c4f1a2b3d5e6f708192a3b4c5d6e7f8",
"status": "QUEUED",
"channel": "voice",
"priority": "CRITICAL",
"idempotencyKey": null,
"timestamp": "2026-09-18T09:15:04.221Z"
}
4. Confirm what happened
Voice calls are delivered asynchronously. Poll the event you just created:
const receipt = await nerve.events.get(eventId);
console.log(receipt.status, receipt.channel, receipt.provider);
// DELIVERED voice termii
A DELIVERED status means a carrier accepted the call for dialling. If every
carrier fails, the event ends FAILED and error_code tells you why — see
Voice error codes.
Make it safe to retry
Voice calls are expensive and a duplicate is annoying, so pass an idempotency key:
await nerve.voice.send(
{ to: { subscriberId: 'user_8f21c', phone: '+2348012345678' }, script: 'Your code is 4827' },
{ idempotencyKey: 'otp-user_8f21c-2026-09-18' },
);
A retry with the same key returns the cached response instead of placing a second call. See Idempotency.
Next
- Voice reference — pacing, E.164, and error codes.
- Configure Voice failover — Termii and Infobip in the dashboard.
- Why Voice OTP is a fallback.