Skip to main content

Voice

Voice places an automated call that reads a text-to-speech script. It is the opt-in channel for one-time passcodes and other prompts that must not sit unread. It is delivered over POST /v1/events/trigger — the same endpoint as every other channel — with overrides.voice present.

Request contract

POST /v1/events/trigger accepts a ProviderOverrides object. Its voice property is the opt-in and the per-request configuration:

FieldTypeRequiredMeaning
overrides.voice.scriptstring | nullNoThe spoken script. Falls back to the workspace voice template when omitted. {{variable}} placeholders are rendered from payload.
overrides.voice.voice_idstring | nullNoA named TTS voice profile. Honoured by Infobip; ignored by Termii. The wire name is snake_case (voice_id).
overrides.voice.languagestring | nullNoA BCP-47 language tag for the voice profile, e.g. en-US.

script, voice_id, and language are all optional and nullable. Supplying the voice object at all — even empty — is what marks the event as a voice event; a recipient merely having a phone number does not make it eligible. The full request schema is in the gateway reference.

{
"name": "otp-login",
"to": { "subscriberId": "user_8f21c", "phone": "+2348012345678" },
"overrides": {
"voice": {
"script": "Your verification code is 4827",
"voice_id": "Ada",
"language": "en-US"
}
}
}

Routing: eligibility versus order

Supplying overrides.voice makes voice eligible for the recipient; it does not, on its own, make the call first. Eligibility and order are separate:

  • Eligible — the recipient has an E.164 to.phone and the request supplies overrides.voice.
  • Attempted first — only when the event priority is CRITICAL or HIGH (the reachability order puts voice ahead of SMS), or when the request sets extraParams.voice_primary to "true" (or "1").
  • Failover otherwise — for a NORMAL or LOW event with neither of those, the cost order applies: SMS is tried first and voice is the fallback if the SMS attempt times out or fails.

The chosen channel is returned as channel on the trigger response, so a routine event that supplied overrides.voice still comes back as "sms". Use CRITICAL/HIGH (or extraParams.voice_primary=true) when you genuinely want the call attempted first.

Phone numbers (E.164)

Voice calls are dialled on the recipient's to.phone, which must be an E.164 number:

  • a leading +
  • the country calling code, then the national number
  • digits only — no spaces, dashes, brackets, or trunk prefixes
ValueValidWhy
+2348012345678Yes+, country code, national number
+14155552671YesSame shape, US number
08012345678NoNo country code or +
+234 801 234 5678NoContains spaces

A recipient with no phone is not eligible for voice. If no supplied contact detail makes any channel eligible, the trigger is rejected with 400 BAD_REQUEST before it is enqueued.

TTS digit pacing

Text-to-speech engines read a long digit run as one cardinal number — "forty-eight thousand two hundred and ninety-one" — which is useless as a passcode. The worker therefore paces any contiguous run of four or more ASCII digits by inserting a short pause between each digit. The pause is written as a comma, which both Termii and Infobip TTS engines understand:

Script you sendScript that is spoken
Your code is 48291Your code is 4, 8, 2, 9, 1
Your code is 4827Your code is 4, 8, 2, 7
Total is 500Total is 500 (fewer than four digits)

Pacing is applied after interpolation, so you write the code the way a human reads it (48291) and the platform inserts the pauses. The transform is idempotent: an already-paced 4, 8, 2, 9, 1 is left untouched.

Pacing also runs over template variables that survive sanitisation, so a code injected as {{code}} is paced exactly like one typed into the script.

Language and voice profile

  • language accepts a BCP-47 tag. en_US is normalised to en-US, a bare en resolves to en-US, and an omitted tag defaults to en-US.
  • Infobip reduces the tag to its primary subtag before calling TTS (en-USen); Termii uses its own default.
  • voice_id names a TTS profile and is only honoured by Infobip. On the Termii leg it is ignored, so do not depend on a specific voice for the primary call.

Cost

A voice call is billed at roughly $0.015 — about three times an SMS — which is why the channel is opt-in per event rather than inferred. Cost is reported per message as cost_micro_usd (15000 micro-USD per call).

Delivery receipts and call status

A carrier accepting the call is not the same as the call finishing, so a voice event's lifecycle is completed by the carrier's call-status callback. Point your Termii, Infobip or Twilio voice webhook at https://api.nervly.io/v1/webhooks/{provider}; Nerve verifies the callback's signature (Termii X-Termii-Signature, Infobip X-Hub-Signature, Twilio X-Twilio-Signature) and normalises the carrier's own call state onto the platform's delivery vocabulary.

Carrier call states such as in-progress, ringing, busy and completed are never emitted as statuses of their own. They map onto one of QUEUED, SENT, DELIVERED, FAILED or SUPPRESSED:

Carrier stateTermiiInfobipTwilioDelivery status
Accepted / queuedSent, Sending, Pending, Queued, Accepted, SubmittedPENDING, PENDING_ACCEPTED, PENDING_ENROUTE, ACCEPTEDqueuedQUEUED
Dialling / ringing / answeredIn-Progress, Ringing, Calling, AnsweredRINGING, CALL_IN_PROGRESS, ESTABLISHED, ANSWEREDinitiated, ringing, in-progressSENT
Call completedDelivered, Completed, FinishedDELIVERED, COMPLETED, FINISHEDcompletedDELIVERED
Not reachedBusy, No-Answer, Unanswered, Failed, Rejected, Cancelled, MissedBUSY, NO_ANSWER, UNDELIVERABLE, UNDELIVERED, EXPIRED, REJECTED, FAILED, DISCARDEDbusy, no-answer, failed, canceledFAILED
Blocked before dialDND, SuppressedDND, SUPPRESSEDSUPPRESSED
Anything unrecognisedFAILED

The callback carries the call duration in seconds and the integer cost_micro_usd; the message timeline records both, and the message's cost_micro_usd is updated to the carrier's figure. Nerve stores no recording URL, transcript or plaintext phone number for a voice event: the receipt carries a masked recipient and the carrier's own message id only.

If your carrier cannot present a signature, its callback is refused in production rather than trusted — configure webhook signing (or a shared secret) in the carrier's dashboard before pointing it at Nerve.

Error codes

Trigger-time validation failures use the standard error contract: a 400 with error: "BAD_REQUEST". Once an event is accepted (202), failures are reported on the message record returned by GET /v1/events/{eventId}:

error_codestatusMeaning
DISPATCH_FAILEDFAILEDEvery carrier in the voice chain failed. error_detail names what was tried, e.g. all voice providers failed.
SUPPRESSEDSUPPRESSEDDelivery was withheld before any call — for example the subscriber set channels.voice to false, or a promotional voice event was aimed at a DND-screened number.

A voice event can also fail closed on a scripting error rather than dial an incomplete prompt. If script references a {{placeholder}} that payload does not supply, the call is not placed and the event ends FAILED with error_detail naming the event and the missing keys: voice template "otp-login" references undefined variables: code. Fix the payload and send again with a fresh idempotency key.

Test Mode

A nerve_sk_test_... key short-circuits carrier dispatch: no call is placed, a normal timeline is recorded, and the event reports synthetic success. This is the safe way to exercise the voice path in CI.

Next