ThunderPhone v3 is live.Self-serve, from 2¢/min.Read the announcement

Developer cookbook

Connect over SIP (no phone number)

Send calls to a ThunderPhone agent straight from your PBX, carrier or SIP application, with a one-time SIP URI per call or a permanent SIP address.

If your own system already handles phone numbers (a PBX, a contact-center platform, or a carrier account where you rotate numbers), you can send calls to a ThunderPhone agent over plain SIP. You don't import or buy a number on ThunderPhone. Your system dials a SIP address and the agent answers.

There are two kinds of SIP address:

Per-call SIP URISIP endpoint
Looks likesip:c-<token>@sip.thunderphone.comsip:agent-<id>-<letters>@sip.thunderphone.com
Created withPOST /v1/calls/register-sip, once per callPOST /v1/sip-endpoints, once
AuthenticationThe one-time token in the addressSIP digest username + password (and optionally a source-IP allowlist), a source-IP allowlist alone for platforms that authenticate by IP, or a secret address for providers that can only forward to an address
Good forApps that already call an API before transferring a call; per-call variablesPBXs and carriers that should just "point a trunk" at ThunderPhone
Valid for5 minutes, one callUntil you delete it

Both kinds route to one agent in your organization, and both show up in call history like any other inbound call.

Per-call SIP URI

1. Register the call

curl -X POST https://api.thunderphone.com/v1/calls/register-sip \
  -H "Authorization: Bearer sk_live_YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "agent_id": 12,
    "to_number": "+14155550142",
    "variables": {"customer_name": "Ada"}
  }'
{
  "call_id": 862197007562,
  "sip_uri": "sip:c-1790372992.Qm3xT8vLk2PzR7nYw4HbJa.uF6sD1gK9eW2cX5vB8nM0q@sip.thunderphone.com",
  "expires_at": "2026-09-25T21:49:52Z",
  "agent_id": 12,
  "to_number": "+14155550142"
}
  • to_number (optional) is the number the caller dialed on your side. It is stored as the call's destination in call history, and the agent sees it as its own number.
  • variables (optional) fill {{placeholders}} in the agent's prompt, the same way as for outbound calls.
  • call_id is the id the call will have in call history (GET /v1/calls/{call_id}) once it connects.

2. Dial the URI

Send the call to sip_uri within 5 minutes. The URI works for one call only: once a call has used it, it is spent, even if that call ends quickly.

With Twilio, for example, return this TwiML for the call you want to hand over:

<Response>
  <Dial>
    <Sip>sip:c-1790372992.Qm3xT8vLk2PzR7nYw4HbJa.uF6sD1gK9eW2cX5vB8nM0q@sip.thunderphone.com;transport=tcp</Sip>
  </Dial>
</Response>

From a PBX, dial the URI as a SIP destination. No username or password is needed; the token is the credential, so treat the URI like a password until it has been used.

SIP endpoint (permanent address)

1. Create the endpoint

curl -X POST https://api.thunderphone.com/v1/sip-endpoints \
  -H "Authorization: Bearer sk_live_YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"agent_id": 12, "name": "Main PBX"}'
{
  "id": 7,
  "name": "Main PBX",
  "agent_id": 12,
  "sip_uri": "sip:agent-12-jhxmginwjx@sip.thunderphone.com",
  "auth": "digest",
  "username": "tp409667538f83",
  "password": "8mJ0…",
  "allowed_source_cidrs": [],
  "status": "active",
  "created_at": "2026-09-25T21:46:18Z"
}

2. Point your system at it

Configure a SIP trunk or destination with:

SettingValue
SIP URIthe endpoint's sip_uri
Username / passwordusername / password (digest authentication; ThunderPhone challenges each call). None for an ip_allowlist endpoint (below)
TransportTCP on port 5060 or TLS on port 5061 (recommended), or UDP on port 5060
CodecsG.711 (PCMU/PCMA) or G.722

These transport and codec settings apply to per-call URIs too. Two network details matter for both kinds of address:

  • Follow Record-Route. After the first INVITE, the rest of the call's signalling (ACK, hang-up, transfers) goes to the specific ThunderPhone server that answered, which is named in the Record-Route header (sip-1.thunderphone.com or sip-2.thunderphone.com, or its IP address). If you restrict outbound SIP, allow those hosts as well as sip.thunderphone.com: 172.170.39.202 (sip), 13.86.104.121 (sip-1) and 20.80.98.90 (sip-2).
  • Audio goes elsewhere. sip.thunderphone.com carries signalling only. Allow media (RTP) to the addresses and ports in the SDP answer.

Every call to the address reaches the endpoint's agent, whatever number your caller dialed on your side. You never import numbers, so rotating them on your side needs no change on ThunderPhone.

Pass the dialed number (optional)

To record which of your numbers the caller dialed, send it on the INVITE in an X-Original-To header (for example X-Original-To: +14155550142) or in a standard Diversion header. ThunderPhone stores it as the call's destination.

The destination is chosen in this order: the to_number given when a per-call URI was registered, then a number in X-Original-To, then one in Diversion. If none of these gives a number, it is shown as sip:agent-12-jhxmginwjx (the endpoint's address) for an endpoint, or sip:call for a per-call URI.

With Twilio you can add the header to the SIP URI: sip:agent-12-jhxmginwjx@sip.thunderphone.com;transport=tcp?X-Original-To=%2B14155550142.

Restrict source IPs (optional)

Set allowed_source_cidrs to only accept calls from your own signalling addresses, on top of the username and password:

curl -X PATCH https://api.thunderphone.com/v1/sip-endpoints/7 \
  -H "Authorization: Bearer sk_live_YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"allowed_source_cidrs": ["203.0.113.0/28"]}'

Calls from any other address are refused before they reach the agent. Use the addresses your system sends SIP signalling from (for a carrier such as Twilio, its published signalling IP ranges), not your media addresses.

Platforms that authenticate by source IP

Some contact-center platforms, such as Talkdesk, Amazon Connect and Amazon Chime Voice Connector, identify themselves only by the addresses they send SIP from and document no password authentication. For these, create the endpoint with auth set to ip_allowlist and list the platform's SIP signalling ranges:

curl -X POST https://api.thunderphone.com/v1/sip-endpoints \
  -H "Authorization: Bearer sk_live_YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "agent_id": 12,
    "name": "Amazon Chime",
    "auth": "ip_allowlist",
    "allowed_source_cidrs": ["3.80.16.0/23", "99.77.253.0/24"]
  }'

The response has "username": null and no password. Point the platform at the sip_uri over TLS (port 5061) or TCP (port 5060): an ip_allowlist endpoint refuses every call over UDP, because a UDP source address can be forged.

The list must have at least one entry. Each must be a public IPv4 range of /16 or narrower or an IPv6 range of /48 or narrower. Use the signalling ranges the platform publishes for the regions you use, not its media ranges and not a whole cloud provider's.

To switch an existing endpoint, PATCH it with auth (and allowed_source_cidrs if its list doesn't meet these rules). Switching back to digest returns a new password once.

Providers that can only forward to an address

Some providers' call forwarding takes a SIP address and nothing else: no username, no password, no fixed source addresses. For these, create the endpoint with auth set to secret_address:

{
  "agent_id": 12,
  "name": "Sonetel forwarding",
  "auth": "secret_address"
}

Its address ends in 28 random letters, and that address is the credential. The response returns it in full once, in secret_sip_uri; everywhere else sip_uri shows it masked. Paste it into the provider's forwarding box. Calls over UDP, TCP or TLS are accepted.

Platforms that dial a phone number

Five9, NICE CXone and Zoom Contact Center send a call to a phone number and can't dial the endpoint's agent-… address. Give an ip_allowlist endpoint up to 5 dial_in_numbers: the platform then sends its INVITE to sip:<number>@sip.thunderphone.com and reaches the endpoint's agent. Zoom Contact Center also needs TLS with SRTP, so set srtp to required (SRTP, like dial-in numbers, is available on ip_allowlist endpoints only):

curl -X POST https://api.thunderphone.com/v1/sip-endpoints \
  -H "Authorization: Bearer sk_live_YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "agent_id": 12,
    "name": "Zoom Contact Center",
    "auth": "ip_allowlist",
    "allowed_source_cidrs": ["203.0.113.0/24", "198.51.100.0/24"],
    "dial_in_numbers": ["999123456789012"],
    "srtp": "required"
  }'

Replace the example ranges (documentation addresses, which the API refuses) with your platform's SIP signalling ranges. Enter each number exactly as the platform dials it, with or without the + (Zoom's 15-digit 999… numbers have none); spaces, dashes and parentheses are removed. A number counts as an address, not a password: calls to it are checked against allowed_source_cidrs and must arrive over TCP or TLS, the same as calls to the sip_uri. A number can't already be in use on ThunderPhone, as a phone number or on another SIP endpoint; creating or updating with one returns 409. To change the list, PATCH the endpoint with the full new dial_in_numbers.

Calls to a dial-in number are SIP-address calls: no ThunderPhone number carries them, so they don't appear under phone numbers and have no phone-number charge. The call's destination is the number in X-Original-To or Diversion when the platform sends one, otherwise the dial-in number.

Pass call context in SIP headers

A contact-center platform or PBX usually knows things about the call before it reaches the agent: its own call or contact id, what the caller chose in the IVR, the account number they entered. Send them as SIP headers on the INVITE and the agent can use them.

ThunderPhone reads two kinds of header:

  • every header whose name starts with X- (for example X-Amzn-ConnectContactId or X-Intent), except X-TP-* and X-LK-*, which ThunderPhone's own SIP servers use and are always dropped;
  • User-to-User (UUI). A value marked ;encoding=hex is decoded to text when it decodes to UTF-8; the value as sent is kept as well.

This works on both kinds of SIP address, and on numbers imported through a manual SIP connection. It does not apply to ThunderPhone numbers or to numbers on a Twilio or Telnyx connection.

Use them in the prompt

Each header becomes a call variable named sip_ plus the header name in lower case, with every run of characters other than letters and digits turned into one _:

HeaderVariable
X-Amzn-ConnectContactId{{sip_x_amzn_connectcontactid}}
X-Intent{{sip_x_intent}}
User-to-User{{sip_uui}} (decoded) and {{sip_uui_raw}} (as sent, only when it was hex-encoded)

A header only reaches the agent where its prompt names it; nothing is added to the conversation otherwise. A variable of the same name from register-sip or from your call setup webhook wins over the header. Give each placeholder a default ({{sip_x_intent|unknown}}) so calls without the header still read well.

Limits

At most 32 headers per call, 1,024 characters per value and 8 KB in total; headers past these limits are dropped. Control characters are removed.

Where they show up

  • Call history: GET /v1/calls/{call_id} returns sip_headers (header name in lower case → value), or null for a call without any.
  • Your platform's call id: the first of these headers the call carried is stored as the call's external_call_id: X-Amzn-ConnectContactId (Amazon Connect), x-inin-cnv (Genesys Cloud, with the trunk's Conversation Headers setting on), X-Talkdesk-Interaction-Id (Talkdesk), X-ZOOM-Call-Sid (Zoom Contact Center), X-CallID (Five9), X-Call-ID (set it yourself on any other platform) and X-Genesys-Conversation-Id. When the agent hands the call back to your platform, GET /v1/calls/handoff finds the call by it.
  • Webhooks: the telephony.incoming event includes sip_headers when the call carried some. For numbers on a manual SIP connection, so does the blocking call configuration request and the agent's call setup webhook.

What callers and your system see

  • Caller ID: the user part of the From header is stored as the caller. A phone number (digits, optionally with +, spaces, dashes, dots or brackets) is normalized to E.164; anything else, such as a SIP username, is kept as sent. At most 32 characters are kept, and a missing caller is stored as anonymous.
  • Refused calls: an unknown or expired per-call URI gets SIP 404 Not Found. An already-used per-call URI, a disabled endpoint, or a source outside the allowlist (including any UDP call to an endpoint with an allowlist) is disconnected before the agent starts: your system may see the call fail, or connect and then end straight away. These refusals do not create a call-history entry. If your organization is out of credit or has reached its spending limit, the call is refused and shows in call history as a failed call with no charge. To retry a per-call attempt, register a new URI.
  • Transfers: the agent's cold transfers work (ThunderPhone sends a SIP REFER, which your system must accept). A transfer number can also be a SIP address in your system, such as a queue, with SIP headers for it to read; see Transfer to a SIP address. The agent can also hand the call back by hanging up with routing headers on the BYE; see Return the call to your contact center. Warm transfers are not available on SIP-address calls, because there is no ThunderPhone number to show as caller ID for the second leg. With Twilio, the TwiML above connects the agent but does not handle transfers: add referUrl="https://YOUR_SERVER/sip-transfer" to the <Dial>, and have that webhook read ReferTransferTarget and return TwiML that dials the transfer destination (see Twilio's SIP REFER guide).
  • Text messages: the SIP address and the dialed number can't send texts. The agent can text during a SIP-address call only if it has a separate SMS-capable number set up for texting; otherwise its SMS tool is unavailable.
  • Registration: ThunderPhone does not accept SIP REGISTER. Send calls to the address; don't register to it.

Pricing

SIP-address calls are billed like any other call. There is no phone-line part, because no ThunderPhone number carries the call. Your own carrier bills its side of the call as usual. See Pricing.

Test it

  1. Create a per-call URI or an endpoint as above.
  2. Dial it from your system, or from any SIP softphone (for a digest endpoint, enter the username and password; an ip_allowlist endpoint only answers calls from its allowed ranges).
  3. After hanging up, open the call in Calls in the dashboard, or fetch it with GET /v1/calls/{call_id}. It shows the caller, the destination (see the order under "Pass the dialed number") and the transcript.

API details: SIP addresses API reference.