VEXYL AI

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.