Skip to main content

Configure Voice failover (Termii → Infobip)

Nervly places Voice OTP calls through a two-carrier chain: Termii first, then Infobip. If Termii fails or stalls, Infobip places the call within the same dispatch, so your code sees one event and one delivery receipt regardless of which carrier answered.

Both carriers are Bring-Your-Own: you add your own credentials in the dashboard. This how-to takes about five minutes.

Voice calls carry a caller-ID binding: the number the recipient sees is resolved from a caller ID you configure as a sender identity for the workspace, never inferred from the recipient. Voice is provider-gated on this — only Infobip honours a caller ID today. A per-message overrides.voice.caller_id is validated against the caller IDs your workspace owns (anti-spoof); one that names a caller ID the workspace does not own is refused at dispatch with SENDER_NOT_ALLOWED, and a call with no eligible caller-ID-capable carrier is refused rather than dialled from an arbitrary number.

1. Add the Termii credential​

  1. Open the dashboard and go to Providers.
  2. Add a Termii provider and paste the API key from your Termii account. Voice uses the same Termii credential as SMS — there is no separate voice key.
  3. Set its priority to 1 so it is tried first.
  4. Click Test Connection to verify the credential before real traffic.

Termii carries the primary call. It does not accept a named voice profile, so voice_id is ignored on this leg — the carrier uses its default voice. It has no caller-ID configuration surface either, so a caller_id binding cannot be presented on this leg.

2. Add the Infobip credential​

  1. Add an Infobip provider with your Infobip API key and base URL.
  2. Set its priority to 2 so it is the fallback.
  3. Click Test Connection.

Infobip is the failover leg and the only one of the two that honours voice_id, so name a TTS voice if you want a specific one. It is also the only leg that honours caller_id, so name the caller ID you configured if you want a specific number on the call.

Priority order is per workspace and per channel

priority controls failover across all channels. A credential used for both SMS and Voice keeps the same order. See Provider credentials for the full model.

3. What failover actually does​

Voice is opt-in per event: a call is only eligible when the request supplies overrides.voice (the SDK's voice.send always does). Eligibility is not the same as being tried first:

  • Voice is attempted first only when the event priority is CRITICAL/HIGH or the request sets extraParams.voice_primary to "true"/"1".
  • For a routine (NORMAL/LOW) event without that flag, the cost order applies: SMS is tried first, and voice is the failover once the SMS attempt times out or fails.

Once voice is the channel being attempted:

  • Each carrier attempt gets a 5-second budget. A carrier that accepts the connection but never answers is treated as failed rather than left to ring.
  • The first success wins. The receipt names the carrier that placed the call as provider, so you never have to guess which leg ran.
  • If both carriers fail, the event ends FAILED with error_code DISPATCH_FAILED and the carriers that were tried in error_detail.

Each carrier also has its own per-workspace circuit breaker (breaker:{workspace_id}:{provider}:state). When a carrier's breaker trips OPEN, that carrier is refused without a network round trip and the chain moves straight to the next one; after the cool-off it goes HALF_OPEN, admits a single probe, and either closes on success or re-opens. See Failover & circuit breakers for the thresholds.

You do not implement this retry yourself — Nervly has already tried both carriers. Retrying a 202-accepted event only risks a duplicate call.

4. Provider differences to know​

BehaviourTermiiInfobip
RolePrimaryFallback
voice_id (TTS profile)IgnoredHonoured
caller_id (caller ID)Not supportedHonoured
languageBCP-47 tagReduced to the primary subtag (en-US → en)
CredentialAPI keyAPI key, base URL

For the full request contract, see the Voice reference.

5. Place the call first​

In Test Mode (nervly_sk_test_...) neither carrier is dialled: the worker records a normal timeline and returns a synthetic success, so you can exercise the chain without cost. When you are ready for a real call, send a CRITICAL event (or set extraParams.voice_primary=true) so voice is attempted first rather than ordered behind SMS:

# CRITICAL puts voice at the head of the order; the event id comes back in the 202.
curl -X POST https://api.nervly.io/v1/events/trigger \
--connect-timeout 5 \
--max-time 10 \
-H "Authorization: Bearer $NERVLY_API_KEY" \
-H "X-Priority-Override: CRITICAL" \
-H "Content-Type: application/json" \
-d '{
"name": "otp-login",
"to": { "subscriberId": "user_8f21c", "phone": "+2348012345678" },
"overrides": {
"voice": {
"script": "Your verification code is 4827. It expires in five minutes.",
"voice_id": "Ada",
"language": "en-US"
}
}
}'

6. Read which carrier placed the call​

Poll the event and read channel and provider from the receipt. termii means the primary carrier answered; infobip means voice failover ran:

curl https://api.nervly.io/v1/events/evt_9c4f1a2b3d5e6f708192a3b4c5d6e7f8 \
--connect-timeout 5 \
--max-time 10 \
-H "Authorization: Bearer $NERVLY_API_KEY" \
| jq -r '"channel=\(.channel) provider=\(.provider) status=\(.status)"'
{
"event_id": "evt_9c4f1a2b3d5e6f708192a3b4c5d6e7f8",
"event_name": "otp-login",
"subscriber_id": "user_8f21c",
"status": "DELIVERED",
"channel": "voice",
"provider": "infobip",
"priority": 1,
"attempts": 2,
"cost_micro_usd": 15000,
"test_mode": false,
"variables_keys": [],
"created_at": "2026-09-11T21:00:00.123Z",
"updated_at": "2026-09-11T21:00:12.456Z"
}

Read channel before provider: provider uses the same Termii/Infobip names for SMS as for voice, so provider: 'termii' alone does not prove a call was placed. Only once channel: 'voice' does the carrier tell you about the call — provider: 'termii' means the primary voice carrier answered, and provider: 'infobip' means voice failover ran, worth a look at the Termii credential or its account balance. A receipt with channel: 'sms' is an SMS dispatch, not a voice call.

Next​