Handle SIP phone calls with Speech to Speech
Handle SIP phone calls with Speech to Speech
Route PSTN, contact-center, or PBX calls into a Speech to Speech session. Register a Direct SIP number, verify the incoming-call webhook, then open wss://api.x.ai/v1/realtime?call_id=… with your API key.
Register the number
POST /v2/phone-numbers with origin: "byo_trunk" for a customer-owned E.164 number. Include the webhook URL that should receive realtime.call.incoming events. xAI returns the webhook signing secret once — store it. Provisioning xAI-owned numbers via API is not supported.
Point your carrier at sip:{number}@sip.voice.x.ai;transport=tls. Twilio, Telnyx, and Plivo all work; set allowed_addresses to your provider's SIP signaling CIDRs if you use IP allowlists.
Incoming webhook → WebSocket
When a caller dials in, verify webhook-id, webhook-timestamp, and webhook-signature, then read data.call_id. Open the realtime socket with that call_id, send session.update (voice, instructions, turn detection), then response.create so the agent speaks first.
import asyncio, json, os, websockets
async def handle_sip_call(call_id: str):
async with websockets.connect(
f"wss://api.x.ai/v1/realtime?call_id={call_id}",
additional_headers={"Authorization": f"Bearer {os.environ['XAI_API_KEY']}"},
) as ws:
await ws.send(json.dumps({
"type": "session.update",
"session": {
"voice": "eve",
"instructions": "You are a helpful phone support agent.",
"turn_detection": {"type": "server_vad"},
},
}))
await ws.send(json.dumps({"type": "response.create"}))
async for msg in ws:
print(json.loads(msg)["type"])
asyncio.run(handle_sip_call("00000000-0000-0000-0000-000000000000"))
Ephemeral client secrets are not supported on SIP call_id sessions. Authenticate with the long-lived API key on your backend.
Call control and DTMF
- Transfer:
POST /v1/realtime/calls/{call_id}/referwith{"target_uri":"sip:agent@example.com"}or atel:URI. A200with{}means the destination answered.502means the downstream rejected; the same session stays live and the caller hears dialtone during the pending REFER. - Hang up:
POST /v1/realtime/calls/{call_id}/hangup. - DTMF keypresses buffer automatically on SIP only. Digits flush to the model on
#, after 2.5s idle, or when the caller starts speaking. You also getinput_audio_buffer.dtmf_event_receivedaudit events.
Resume across calls
Set session.resumption.enabled: true on the first call and save that call_id. On a later call open wss://api.x.ai/v1/realtime?call_id={new}&conversation_id={saved} with the same resumption flag. History expires after 30 minutes of inactivity. Replayed turns arrive as conversation.item.created.
Pitfalls
- Codecs: G.711 μ-law, A-law, or G.722 on the trunk.
- Never put the API key in a browser client for SIP joins.
- Console API credits are a separate bill from SuperGrok's weekly pool on grok.com.