VOICE / handle-sip-phone-calls

Voice

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}/refer with {"target_uri":"sip:agent@example.com"} or a tel: URI. A 200 with {} means the destination answered. 502 means 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 get input_audio_buffer.dtmf_event_received audit 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.