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

Developer cookbook

Bring your own numbers (VoIP, API)

Connect Twilio or Telnyx, or SignalWire, Vonage, Plivo and any other SIP trunk over SIP, import the phone numbers you already own, and let ThunderPhone agents answer and place calls on them.

Approved ThunderPhone numbers can place outbound calls and run campaigns. If you prefer to use numbers you own through your own VoIP provider, this guide walks you through the connection flow: test credentials → create connection → import numbers → verify.

Supported providers

Providerprovider idNotes
TwiliotwilioAccount SID + Auth Token; one connection per Twilio account or subaccount (see Twilio subaccounts)
TelnyxtelnyxAPI key; guided onboarding available (setup_method: guided_telnyx)
SignalWiresignalwireNo native provider connection; connect it as manual with the SignalWire SIP preset
VonagevonageNo native provider connection; connect it as manual with the Vonage SIP preset
Manual SIPmanualAny SIP trunk, including SignalWire, Vonage and Plivo. If you start from one of those provider tiles in the dashboard, the connection is still manual and stores the choice in sip_config.preset (signalwire, vonage or plivo)

1. Test credentials

Before you create a persisted VoIP connection, probe the provider credentials to confirm they work. This returns a verification_evidence_id that you pass to the create step so the credentials aren't double-charged for testing.

curl -X POST https://api.thunderphone.com/v1/voip-connections/test \
  -H "Authorization: Bearer sk_live_YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "provider":    "telnyx",
    "credentials": { "apiKey": "KEY...", "connectionId": "123456" },
    "sip_config":  { "domain": "acme.sip.telnyx.com" }
  }'
Response
{
  "status": "pass",
  "verification_evidence_id": "b9a2...",
  "suggested_connection_name": "Telnyx: Acme Main (+15550001234)",
  "checks": {
    "credentials_valid": true,
    "inbound_reachable": true,
    "outbound_authorized": true
  }
}

If any check fails, response status will be fail and checks will show which step blew up. Fix the provider-side config (trunk assignment, IP allowlist, outbound authorization) and retry.

2. Create the connection

Pass the verification_evidence_id you just got:

curl -X POST https://api.thunderphone.com/v1/voip-connections \
  -H "Authorization: Bearer sk_live_YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name":         "Acme Telnyx Main",
    "provider":     "telnyx",
    "setup_method": "api_key",
    "credentials":  { "apiKey": "KEY...", "connectionId": "123456" },
    "sip_config":   { "domain": "acme.sip.telnyx.com" },
    "verification_evidence_id": "b9a2..."
  }'

The response is a VoipConnection object in status="connected". Credentials and other secret connection values are stored server-side and never returned in plain text by subsequent GETs — to rotate, run a fresh test and PATCH with the new evidence.

Twilio subaccounts

A Twilio connection is bound to the single Twilio account whose Account SID and Auth Token it holds. Twilio keeps phone numbers and SIP trunks inside each subaccount, so a connection made with the parent account only ever sees the parent's own numbers, and an existing connection cannot be switched to a different subaccount later.

To reach numbers held in subaccounts, create one connection per subaccount. The dashboard does this for you: when you connect a parent account that has active subaccounts, the setup dialog lists them, you tick the ones you want, and ThunderPhone creates a connection (and a SIP trunk) in each one. The same flow is available over the API:

# Discover active subaccounts visible to the parent credentials
curl -X POST https://api.thunderphone.com/v1/voip-connections/twilio/subaccounts \
  -H "Authorization: Bearer sk_live_YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"credentials": {"accountSid": "ACparent...", "authToken": "..."}}'
Response
{
  "discovery_id": "7c1e...",
  "parent": { "sid": "ACparent...", "friendly_name": "Acme" },
  "subaccounts": [
    { "sid": "ACsupport...", "friendly_name": "Acme Support", "already_connected": false, "connection_id": null },
    { "sid": "ACsales...",   "friendly_name": "Acme Sales",   "already_connected": true,  "connection_id": 5 }
  ]
}
# Create a connection for each selected subaccount
curl -X POST https://api.thunderphone.com/v1/voip-connections/twilio/subaccounts/connect \
  -H "Authorization: Bearer sk_live_YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"discovery_id": "7c1e...", "subaccount_sids": ["ACsupport..."]}'

The response carries one row per subaccount with status of created, skipped (already connected) or failed plus an error you can act on. Rows are independent, so a failure in one subaccount never blocks the others, and a discovery_id stays valid for 30 minutes so a failed row can simply be retried. Subaccount tokens are read from Twilio at discovery time and stored on the new connection; they are never returned by the API. Each new connection then imports and verifies numbers exactly like a connection you created by hand. Add "enable_transfers": true to also turn call transfers on for each new connection's SIP trunk; a failure there is reported in that connection's test_result.transfer_enable_error and does not fail the row.

3. List and import numbers

Inspect the numbers visible to your credentials that aren't already in a ThunderPhone org:

curl https://api.thunderphone.com/v1/voip-connections/5/available-numbers \
  -H "Authorization: Bearer sk_live_YOUR_API_KEY"

Then import the ones you want:

curl -X POST https://api.thunderphone.com/v1/voip-connections/5/import-numbers \
  -H "Authorization: Bearer sk_live_YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"numbers": ["+15550001234", "+15550009999"]}'

Each import becomes a phone number resource in your org with number_type="voip" and status="provisioning".

4. Verify each imported number

Importing registers the number as available; actually routing calls through it needs verification. For provider connections (Twilio, Telnyx) that re-checks the provider credentials and SIP reachability. For a manual SIP connection it first tries a loopback self-call: ThunderPhone places a short probe call from the number, through your SIP trunk, back to the same number. If that does not verify the inbound path, it falls back to a short probe call from the number, through your SIP trunk, to a ThunderPhone number. These tests check the trunk's username, password, transport, outbound routing, and inbound delivery. Your carrier bills these calls like any other.

curl -X POST https://api.thunderphone.com/v1/phone-numbers/{id}/verify-voip \
  -H "Authorization: Bearer sk_live_YOUR_API_KEY"

On success, voip_verification_status flips to verified and the number enters status="active". A number in your own Telnyx account keeps taking inbound calls when its test call fails or outbound is not set up yet: it becomes status="active" with voip_verification_status set to unverified, and the response says why. On failure, the response spells out what failed — for a manual trunk that is the carrier's SIP answer, for example "rejected ThunderPhone's credentials (SIP 401)" or "could not route a call (SIP 404)" — fix it (credentials, allowed source IPs, a missing trunk assignment in the provider dashboard) and call again.

For an allowlisted manual SIP connection, the incoming-call test also fails if those Allowed Source IPs would block a real inbound call.

5. Assign agents and take a call

With the number verified, you assign inbound / outbound agents the same way as a ThunderPhone number. See Handle inbound calls and Place outbound calls.

Rotating credentials

When a provider key rotates, re-run the test-then-update flow:

# 1. Test the new credentials
curl -X POST https://api.thunderphone.com/v1/voip-connections/test ...
 
# 2. PATCH the connection with the new evidence
curl -X PATCH https://api.thunderphone.com/v1/voip-connections/{id} \
  -H "Authorization: Bearer sk_live_YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "credentials": { "apiKey": "NEW_KEY..." },
    "verification_evidence_id": "fresh-evidence-id"
  }'

The connection stays in place — no need to re-import numbers.


Next steps