VOICE / register-a-byo-sip-phone-number

Voice

Register a BYO SIP phone number

Create a Direct SIP registration for a customer-owned E.164 line so inbound PSTN or PBX calls can reach Speech to Speech on your team. Official Voice REST documents POST https://api.x.ai/v2/phone-numbers with required origin, name, and either a webhook object or an agent_id, and the SIP Phone Calls guide tells you to use origin: "byo_trunk" for customer-owned numbers while warning that provisioning SpaceXAI-owned numbers via API is not supported. The create response returns dispatch_signing_secret once, so store that secret before you lose the payload.

What you need

You need an xAI inference API key, the E.164 number you already own at a carrier, a publicly reachable HTTPS webhook URL for realtime.call.incoming unless you route with agent_id instead, and a plan for SIP auth using digest username/password or allowed_addresses CIDR allowlists. Neighboring jobs include Connect a Twilio Elastic SIP trunk to xAI for carrier routing, List registered SIP phone numbers to recover phone_number_id later, and Handle SIP phone calls with Speech to Speech for the webhook → WebSocket join path; more voice jobs live on the Voice hub.

Create the registration

Export the key outside source control, then POST the BYO trunk body with webhook fields when your app handles inbound events, remembering that agent_id and webhook are mutually exclusive per the REST reference.

export XAI_API_KEY="your_api_key"

curl -s -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": "Support SIP trunk",
    "phone_number": "+18005550199",
    "allowed_addresses": ["203.0.113.0/24"],
    "webhook": {
      "url": "https://example.com/webhooks/xai-sip"
    }
  }'
import json
import os

import requests

response = requests.post(
    "https://api.x.ai/v2/phone-numbers",
    headers={
        "Authorization": f"Bearer {os.environ['XAI_API_KEY']}",
        "Content-Type": "application/json",
    },
    json={
        "origin": "byo_trunk",
        "name": "Support SIP trunk",
        "phone_number": "+18005550199",
        "allowed_addresses": ["203.0.113.0/24"],
        "webhook": {"url": "https://example.com/webhooks/xai-sip"},
    },
)
response.raise_for_status()
payload = response.json()
print(json.dumps(payload, indent=2))
# Save phone_number.phone_number_id and webhook.dispatch_signing_secret once.

Expect phone_number.phone_number_id, E.164 phone_number, sip_host (typically sip.voice.x.ai), optional sip_auth.auth_username when digest was set, and webhook.dispatch_signing_secret only on this create response. For digest auth, send sip_auth.auth_username together with sip_auth.auth_password because the password is never returned on later list reads, then point your carrier at sip:{number}@sip.voice.x.ai;transport=tls after create succeeds.

Secure the inbound trunk

If you set allowed_addresses, include your provider’s SIP signaling CIDR ranges so INVITEs are accepted; if you set digest credentials, configure the same username and password on the carrier because SpaceXAI never returns the password after creation. Verify webhook signatures with the one-time signing secret before you join call_id over WebSocket, and remember that re-creating the same E.164 after delete requires a fresh POST and a new secret.

Pitfalls

Using origin: "xai_provisioned" while expecting API-provisioned xAI numbers fails the supported BYO path because docs say API provisioning of those numbers is not supported. Omitting both webhook and agent_id leaves no dispatch target for inbound calls, and losing dispatch_signing_secret forces you to rotate webhook configuration because the secret is not recoverable from list. Treating create as hangup or delete confuses inventory setup with live call_id control on /v1/realtime/calls/{call_id}/….