Grok (xAI) Setup
Unlike the OpenAI integration, there is no dedicated Grok trunk type in Vodia PBX. The Grok SIP path is configured as a generic SIP gateway trunk pointed at xAI's SIP ingress, combined with a phone number registered against your xAI account via their API.
Tip: You will need an xAI account with API access and credits loaded before proceeding.
How this differs from OpenAI
| OpenAI | Grok (xAI) | |
|---|---|---|
| SIP destination | sip:{project_id}@sip.api.openai.com | sip:{number}@sip.voice.x.ai;transport=tls — a phone number you register is the SIP user |
| Trunk type | Dedicated OpenAI trunk type in Vodia | Generic SIP gateway trunk |
| Accepting the call | POST /v1/realtime/calls/{call_id}/accept before opening the WebSocket | No accept step. Opening the WebSocket on the call_id is what answers the call |
| Number provisioning | Not applicable | Registered via POST /v2/phone-numbers before any call can be made |
Step 1 — Create an xAI API key
Log into the xAI Cloud Console and create an API key. You'll use it both for number registration below and inside the Voice Agents script.
export XAI_API_KEY="your-key-here"
Step 2 — Register a phone number with xAI
xAI does not provision numbers. You register a number under origin: "byo_trunk", and xAI matches inbound SIP against it. The number does not need to be dialable on the PSTN — it is only ever used between your PBX and xAI — but it must be a well-formed E.164 number, since xAI validates the format.
curl -X POST "https://api.x.ai/v2/phone-numbers" \
-H "Authorization: Bearer $XAI_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"origin": "byo_trunk",
"name": "Vodia PBX voice agent",
"phone_number": "+1XXXXXXXXXX",
"webhook": {
"name": "Vodia PBX webhook",
"url": "https://your-pbx-fqdn/grok"
},
"sip_auth": {
"allowed_addresses": ["YOUR.PBX.PUBLIC.IP/32"]
}
}'
Two fields matter beyond the obvious:
webhook.urlpath must match the dialplan pattern used incall.dial(). If the script callscall.dial('grok'), the webhook URL must end in/grok. This is the binding between an outbound call and the script that receives xAI's callback — a mismatch here is the most common cause of a call that rings forever and never answers.sip_auth.allowed_addressesmust be the address xAI actually sees, not necessarily the address your PBX listens on. On a NAT'd or reserved-IP host, confirm with:run from the PBX itself, and use that address rather than assuming.curl -s https://api.ipify.org; echo
The response includes a webhook signing secret, returned only once:
{
"phone_number": { "phone_number_id": "phone_...", "phone_number": "+1XXXXXXXXXX", "..." : "..." },
"webhook": { "webhook_id": "webhook_...", "dispatch_signing_secret": "whsec_..." }
}
Store it securely — it cannot be retrieved again afterward.
Vodia's Voice Agents JavaScript environment does not expose an HMAC primitive, so this signature cannot be verified inside a script. Treat the webhook URL itself as a shared secret, and restrict it at the firewall to xAI's outbound ranges if stronger assurance is required.
Step 3 — Create the Groktrunk
Create a new Grok trunk in Vodia PBX for connection to Grok
Some trunk configurations normalize the outgoing number to national format (dropping the country code) by default. xAI matches on the exact registered E.164 string, including the leading +. Confirm the trunk's number-format setting is global/E.164, not national — a mismatch here produces a 407/401 challenge from xAI's SIP ingress that looks like an authentication failure but is actually a routing mismatch.
Step 4 — Create the dialplan entry
Pattern: grok
Replacement: 1XXXXXXXXXX (the exact number from Step 2, no need to include + as the trunk will add it)
Trunk: the trunk created in Step 3
A script then reaches Grok with:
call.dial('grok')
which the dialplan rewrites into sip:1XXXXXXXXXX@sip.voice.x.ai;transport=tls.
Step 5
Under tenant general setting make sure the prefix grok is set for URL Prefix for Incoming AI Webhooks. You can have multiple prefixes with space as delimiter for eg openai grok
Step 6 — Verify
Place a test call through a script that only logs its call.http() argument and calls call.dial('grok'). In the SIP trace you should see:
- An INVITE to
sip:+1XXXXXXXXXX@sip.voice.x.ai;transport=tls - A
180 Ringing - Within a second or two, a
realtime.call.incomingwebhook containingdata.call_id
The call continues ringing until a script opens a WebSocket on that call_id — there is no separate accept step. See the Grok basic call transfer example for the full script.
Troubleshooting
| Symptom | Likely cause |
|---|---|
407/401 on the INVITE | R-URI doesn't exactly match the registered number (national vs. E.164 format), or the source IP isn't in allowed_addresses |
404 Not Found on the INVITE | Dialplan replacement is empty or malformed |
| Webhook never arrives | Webhook URL unreachable from the internet, or DNS/TLS misconfigured on the PBX |
| Webhook arrives, but the script never sees it | The webhook path doesn't match the call.dial() string used to place the call — the two must be the same word |
| Call rings indefinitely | Expected until a script opens the WebSocket on call_id — this replaces OpenAI's explicit accept step |
Your Grok integration is now configured and ready to use by your Voice Agents JS script.