Outbound SIP calls and per-call settings
This chapter places outbound calls over SIP, and shows how to choose the speech provider, voice, model, recogniser, language and bot for one call from the dialplan or the outbound request. It also lists which SIP headers the gateway reads and sends. Every accepted value is in Per-call settings reference. The gateway side of SIP is in SIP trunk; the HTTP endpoints are in HTTP API.
Outbound calls over the SIP bridge
When TELEPHONY_MODE is sip_bridge or both, POST /outbound/initiate dials the number itself over SIP. No originate webhook is needed.
OUTBOUND_ENABLED=true
TELEPHONY_MODE=sip_bridge
SIP_PORT=5080
SIP_ALLOWED_IPS=<pbx-or-carrier-ip>
SIP_CODEC=8
SIP_FROM_USER=<your-caller-id>
The gateway needs a place to send the INVITE. It uses the first that applies:
You send phoneNumber as |
The call goes to | Needs |
|---|---|---|
number@host, for example 9198xxxxxxxx@<pbx-ip>:5060 |
That address directly | Nothing more. The host must accept calls from the gateway's IP |
| A bare number | number@ the registrar in SIP_REGISTER_URI |
An active registration (see Registering to a carrier) |
| A bare number, no registrar | number@ SIP_OUTBOUND_PROXY |
SIP_OUTBOUND_PROXY set to the PBX or carrier address |
If none applies, the request fails with Cannot route outbound call: set SIP_REGISTER_URI, SIP_OUTBOUND_PROXY, or use a full SIP address (number@host). If a registrar is configured but not registered yet, it fails with SIP not registered. A digest challenge from the far end is answered with SIP_AUTH_USER and SIP_AUTH_PASSWORD.
Caller ID. callerId in the request, else SIP_FROM_USER, goes into the From header, P-Asserted-Identity and Remote-Party-ID. Many carriers only accept caller IDs they have assigned to you.
Headers the gateway adds. Every outbound INVITE carries X-Session-UUID: <session id>, and X-Bot-Id: <bot> when the request names a bot. A PBX in the path can read them, for example in Asterisk ${PJSIP_HEADER(read,X-Session-UUID)}, to tag a recording or post details back to /session/<uuid>/metadata.
The request waits for the call. Over SIP, POST /outbound/initiate returns once the callee answers or the call fails, so set your HTTP client's timeout to cover the ring time. A success looks like this:
{ "sessionUuid": "550e8400-e29b-41d4-a716-446655440000", "status": "initiated", "transport": "sip", "message": "Outbound call placed via SIP" }
A failure returns HTTP 500 with "status": "failed" and the SIP error, and GET /outbound/status/<uuid> shows failed.
Examples
Through a PBX trunk, with the PBX choosing the carrier:
curl -s -X POST http://127.0.0.1:8081/outbound/initiate \
-H 'Content-Type: application/json' \
-d '{"phoneNumber":"09198xxxxxxxx@<pbx-ip>:5060","callerId":"<your-caller-id>","language":"ml-IN","greetingMode":"dynamic","metadata":{"botId":"reminders"}}'
Through a carrier the gateway is registered to, with a different voice and recogniser for this call:
curl -s -X POST http://127.0.0.1:8081/outbound/initiate \
-H 'Content-Type: application/json' \
-d '{"phoneNumber":"9198xxxxxxxx","language":"hi-IN","metadata":{"botId":"collections","tts_provider":"sarvam","tts_voice":"aparna_hi_kyc","stt_provider":"sarvam"}}'
On the PBX side of the first example, the trunk from the gateway must accept the call and route the dialled number out, as for any SIP extension or trunk. In Asterisk that is the context on the [vexyl-gw] endpoint in Customer PBX side.
Outbound calls through Asterisk (AudioSocket)
With TELEPHONY_MODE=audiosocket the gateway does not dial. It stores the session and posts to your OUTBOUND_ORIGINATE_ENDPOINT:
{ "sessionUuid": "550e8400-e29b-41d4-a716-446655440000", "phoneNumber": "+9198xxxxxxxx", "language": "ml-IN", "timestamp": "2026-10-24T12:00:00.000Z" }
Your endpoint asks Asterisk to place the call, for example with an AMI Originate, and hands the session id to a context that connects the answered call to the gateway:
Action: Originate
Channel: PJSIP/+9198xxxxxxxx@<carrier-endpoint>
Context: vexyl-outbound
Exten: s
Priority: 1
Variable: SESSION_UUID=550e8400-e29b-41d4-a716-446655440000
CallerID: <your-caller-id>
Async: true
[vexyl-outbound]
exten => s,1,NoOp(Outbound bot call ${SESSION_UUID})
same => n,AudioSocket(${SESSION_UUID},127.0.0.1:8080)
same => n,Hangup()
Use the sessionUuid the gateway gave you, never a new one: the bot, voice and greeting were stored under it. OUTBOUND_TIMEOUT (default 10000 ms) caps the wait for your endpoint.
Per-call settings
These session metadata keys change one call only. Anything you do not set comes from .env.
| Key | Values | Effect |
|---|---|---|
tts_provider |
sarvam, google, gemini, elevenlabs, deepgram, azure, murf, vexyl, smallest, openrouter |
Speech provider for the call |
tts_voice |
A voice id of that provider | Voice for the call. See Per-call voice and model for each provider's id form |
tts_model |
vendor/name for openrouter, a bare id for gemini |
Model for providers that take one |
stt_provider |
sarvam, sarvam-r, groq, gemini, deepgram, openai, google, vexyl-stt |
Recogniser for the call |
language_code |
For example ml-IN, hi-IN, en-IN |
Call language (outbound: the language field) |
botId |
A bot id | Which bot answers. An unknown id falls back to the default bot |
realtimeProvider |
litebot, gemini-live, off |
Speech-to-speech instead of the pipeline, or force the pipeline |
greeting_mode, initial_greeting_audio |
See Conversation behaviour | How the call opens |
llm_context |
Free text | Background the bot gets for this call |
caller_id, caller_name, destination_number |
Text | Passed to the bot and shown in logs and webhooks |
An unknown tts_provider or stt_provider is dropped with a warning in the log (Invalid tts_provider or Invalid stt_provider) and the .env default is used. Set tts_voice and tts_model together with the tts_provider they belong to: only that provider reads them.
Where to set them
| Call type | How |
|---|---|
| Inbound, Asterisk over AudioSocket | POST the keys to /session/<uuid>/metadata before AudioSocket() (Send caller details first) |
| Inbound, PBX to the SIP bridge | POST the keys under a UUID, then Dial() with the header X-Session-UUID: <that uuid> (Passing the session id and bot) |
| Outbound, any mode | Top-level fields or inside metadata in POST /outbound/initiate |
| Inbound, PBX to the SIP bridge, no HTTP | Custom SIP headers such as X-Bot-Id and X-TTS-Voice on the INVITE (Per-call settings reference) |
| Inbound from a PBX that can neither post nor add headers | Not per call. One set of .env defaults per trunk or gateway instance (Which bot answers) |
Dialplan: choose settings from the call
Asterisk can decide the values from the dialled number, an IVR choice or a database, then post them. This example lets the caller press 1 for Malayalam or 2 for Hindi, and picks a recogniser and voice to match:
[bot-language-menu]
exten => s,1,Answer()
same => n,Set(SESSION_UUID=${TRIM(${SHELL(uuidgen)})})
same => n,Read(LANG_CHOICE,custom/press-1-malayalam-2-hindi,1,,1,5)
same => n,GotoIf($["${LANG_CHOICE}" = "2"]?hindi)
same => n,Set(VX_LANG=ml-IN)
same => n,Set(VX_TTS=sarvam)
same => n,Set(VX_VOICE=shilpa_hi_narration)
same => n,Set(VX_STT=sarvam)
same => n,Goto(post)
same => n(hindi),Set(VX_LANG=hi-IN)
same => n,Set(VX_TTS=sarvam)
same => n,Set(VX_VOICE=aparna_hi_kyc)
same => n,Set(VX_STT=sarvam)
same => n(post),Set(META=caller_id=${URIENCODE(${CALLERID(num)})}&channel=${URIENCODE(${CHANNEL})}&language_code=${VX_LANG}&tts_provider=${VX_TTS}&tts_voice=${VX_VOICE}&stt_provider=${VX_STT}&botId=support)
same => n,Set(CURL_RESULT=${CURL(http://127.0.0.1:8081/session/${SESSION_UUID}/metadata,${META})})
same => n,AudioSocket(${SESSION_UUID},127.0.0.1:8080)
same => n,Hangup()
The prompt file and voice ids are examples; use voices your provider account has. For the SIP bridge instead of AudioSocket, replace the AudioSocket() line with the Dial() and pre-dial header handler from Inbound route to the bot.
To keep per-number settings out of the dialplan, store them in the Asterisk database and read them back, for example Set(VX_VOICE=${DB(vexyl/${EXTEN}/voice)}) after database put vexyl 5550101/voice shilpa_hi_narration on the CLI.
Check what the gateway stored with GET /session/<uuid>/metadata. Each POST also writes a log line, Session metadata stored for <uuid>, that names the language and any TTS: and STT: provider you set.
What SIP headers can carry
| Header | Inbound INVITE (to the gateway) | Outbound INVITE (from the gateway) |
|---|---|---|
X-Session-UUID |
Read. Links the call to metadata posted under that id | Sent on every call |
X-Bot-Id |
Read. Sets botId |
Sent when the request names a bot |
X-TTS-Provider, X-TTS-Voice, X-TTS-Model |
Read. Set tts_provider, tts_voice, tts_model |
Not sent |
X-STT-Provider |
Read. Sets stt_provider |
Not sent |
X-Language |
Read. Sets language_code |
Not sent |
X-Realtime-Provider |
Read. Sets realtimeProvider |
Not sent |
P-Asserted-Identity, Remote-Party-ID |
Not read; the caller number comes from From | Sent with the caller ID |
Any other X- header |
Logged as SIP custom headers, otherwise ignored |
Not sent |
Header values go through the same checks as the metadata POST. When the INVITE also carries X-Session-UUID with posted metadata, the posted values win and headers only fill the gaps. Every accepted value and full dialplan examples are in Per-call settings reference.
Next: Asterisk over AudioSocket, or HTTP API for every metadata and outbound endpoint.