Open in
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
| Provider | provider id | Notes |
|---|---|---|
| Twilio | twilio | Account SID + Auth Token; one connection per Twilio account or subaccount (see Twilio subaccounts) |
| Telnyx | telnyx | API key; guided onboarding available (setup_method: guided_telnyx) |
| SignalWire | signalwire | No native provider connection; connect it as manual with the SignalWire SIP preset |
| Vonage | vonage | No native provider connection; connect it as manual with the Vonage SIP preset |
| Manual SIP | manual | Any 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" }
}'{
"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": "..."}}'{
"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.