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

Developer

VoIP Connections

Bring your own phone numbers into ThunderPhone from Twilio or Telnyx, or from SignalWire, Vonage, Plivo and any other SIP trunk as manual connections: create VoIP connections, verify credentials, and manage imported numbers.

VoIP connections let you plug your existing telephony provider into ThunderPhone. Once connected, you can list, import, and verify phone numbers from your provider account; imported numbers become Phone number resources you can assign agents to.

Supported providers

Providerprovider idSetup methods
Twiliotwilioapi_key
Telnyxtelnyxapi_key, guided_telnyx
SignalWiresignalwireNot available yet — connect it as manual (the API returns 400 for provider: "signalwire")
VonagevonageNot available yet — connect it as manual (the API returns 400 for provider: "vonage")
Manual (any SIP trunk)manualmanual

Endpoints

MethodPathDescription
GET/v1/voip-connectionsList connections
POST/v1/voip-connectionsCreate a connection
PATCH/v1/voip-connections/{connection_id}Update credentials / name
DELETE/v1/voip-connections/{connection_id}Remove a connection
POST/v1/voip-connections/{connection_id}/enable-transfersTurn call transfers on at the carrier (Twilio)
POST/v1/voip-connections/testTest raw credentials (no persisted connection)
GET/v1/voip-connections/sip-hostThe SIP host your trunk sends inbound calls to (manual connections)
POST/v1/voip-connections/suggest-nameInfer a display name from credentials
POST/v1/voip-connections/twilio/subaccountsDiscover the subaccounts under a Twilio parent account
POST/v1/voip-connections/twilio/subaccounts/connectCreate one Twilio connection per chosen subaccount
GET/v1/voip-connections/{connection_id}/available-numbersList numbers already owned in the provider account
POST/v1/voip-connections/{connection_id}/import-numbersImport selected numbers
GET/v1/voip-connections/{connection_id}/search-numbersSearch purchasable numbers (guided provisioning)
POST/v1/voip-connections/{connection_id}/provision-numberBuy a number through the connection (guided provisioning)

Connection object

{
  "id": 5,
  "name": "Acme Telnyx Main",
  "provider": "telnyx",
  "status": "connected",
  "setup_method": "api_key",
  "inferred_name": false,
  "credentials": { "apiKey": "********", "connectionId": "123456" },
  "sip_config": { "domain": "acme.sip.telnyx.com" },
  "test_result": { "checks": { "credentials_valid": true } },
  "last_tested_at": "2026-04-20T18:24:10.113Z",
  "numbers_imported": 3,
  "provisioning_supported": true,
  "provisioning_message": null,
  "transfer_refused_at": null,
  "transfer_enabled_at": null,
  "relay": null,
  "created_at": "2026-04-20T18:24:10.113Z",
  "updated_at": "2026-04-20T18:24:10.113Z"
}
FieldTypeDescription
idintegerConnection id
namestringDisplay name
providerstringtwilio, telnyx, signalwire, vonage, manual
statusstringpending, testing, connected, error (read-only)
setup_methodstringapi_key, manual, guided_telnyx
inferred_namebooleantrue when we auto-generated the name (read-only)
credentialsobjectReturned with sensitive values masked — any key containing key, token, secret, or password reads "********". Writable on create/update
sip_configobjectProvider-specific SIP config. Writable on create/update; sensitive values masked on read the same way. A manual connection set up from a named provider in the dashboard also carries preset (signalwire, vonage or plivo); it is a label only and does not change routing
test_resultobjectResult payload of the most recent credential test (read-only)
last_tested_attimestamp | nullLast credential test (read-only)
numbers_importedintegerCount of phone numbers imported through this connection (read-only)
provisioning_supportedbooleanWhether guided number provisioning works for this provider (currently Telnyx and Twilio)
provisioning_messagestring | nullWhen unsupported: an explanation, e.g. "Provisioning not yet supported for twilio; use Import."
transfer_refused_attimestamp | nullLatest time the carrier refused a cold transfer on this connection, so ThunderPhone connected the call itself. Cleared when transfers are turned on; a refusal on a call that ended before then does not set it again (read-only)
transfer_enabled_attimestamp | nullWhen call transfers were turned on at the carrier through Turn on call transfers (read-only)
relayobject | nullSet when ThunderPhone's SIP registration relay signs in to the carrier for this connection, for carriers that only deliver calls to a registered device. Enabled by ThunderPhone support. state is registering, registered, refused (the carrier refused the username or password), unreachable, evicted (another device signed in with the same account), relay_down or off; reason_code is the carrier's SIP status or the relay's reason for refused, unreachable and evicted; since is when it entered that state; last_registered_at; relay_ip is the relay's address, which ThunderPhone adds to the connection's inbound allowlist (not part of sip_config.inboundSourceIps, and not removable). Never carries credentials (read-only)

List connections

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

Returns an array of Connection objects.


Create a connection

Creation is a two-phase flow:

  1. Call POST /v1/voip-connections/test with the credentials and get back a verification evidence id if the test passes.
  2. Call this endpoint with the same credentials plus verification_evidence_id — the evidence proves the credentials were already validated, avoiding double-billing for the test.
cURL
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..."
  }'

Request fields

FieldTypeRequiredDescription
providerstringyesProvider id (see table above)
namestringnoDefaults to a provider-inferred value
setup_methodstringnoDefaults to api_key; Telnyx supports guided_telnyx
credentialsobjectyesProvider-specific key/secret/connection fields
sip_configobjectnoExtra SIP config — e.g. {"domain": "..."}
verification_evidence_idUUIDyesId returned by POST /voip-connections/test
enable_transfersbooleannoTwilio only. Also turn call transfers on for the connection's SIP trunk, exactly as Turn on call transfers does. Defaults to false; 400 for other providers

Returns 201 Created with the Connection object. With enable_transfers, a Twilio error never fails the create: the connection is saved without transfers, transfer_enabled_at stays null, and test_result.transfer_enable_error says why. Call Turn on call transfers to try again; success removes transfer_enable_error.

Errors:

StatusCondition
400Provider mismatch (evidence generated for a different provider)
400Evidence reused / stale / missing
422Credentials failed re-verification

Update a connection

cURL
curl -X PATCH https://api.thunderphone.com/v1/voip-connections/5 \
  -H "Authorization: Bearer sk_live_YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"name": "Renamed"}'
FieldTypeDescription
namestring
credentialsobjectRotate credentials. Requires a fresh verification_evidence_id when the merged value differs from what is stored
sip_configobjectManual connections: the full SIP settings. Changing them requires a fresh verification_evidence_id; the connection returns status: "pending" and every number on it drops to provisioning while ThunderPhone rebuilds the trunk pair and re-verifies each number with a probe call in the background (numbers_pending_verification in the response lists them). Numbers return to active only after their probe call succeeds. A change to inboundSourceIps alone (order, blanks and duplicates do not count) on a connected connection is applied to the inbound trunk immediately instead: the connection stays connected, its numbers stay active and verified, and each number's incoming-call test resets to untested. If the trunk cannot be updated the request returns 502 and nothing is saved. A pending or error connection re-verifies as above
verification_evidence_idUUIDRequired whenever credentials or sip_config actually change

Secrets are merged, never echoed: a key containing key, token, secret or password that you send as "********", as an empty string, or omit entirely keeps its stored value; send null to clear it. A request that only echoes the masked read-back is not a credential change and needs no evidence.


Delete a connection

cURL
curl -X DELETE https://api.thunderphone.com/v1/voip-connections/5 \
  -H "Authorization: Bearer sk_live_YOUR_API_KEY"

Returns 204 No Content. Phone numbers imported through the deleted connection are not removed — they stay in your org with voip_connection_id cleared.


Turn on call transfers

Twilio trunks refuse cold transfers until the trunk allows them. This sets the connection's Twilio SIP trunk to TransferMode=enable-all and leaves the trunk's caller-ID setting as it is. With Twilio's default (TransferCallerId=from-transferee), the transferred call shows the number of the person being transferred (the caller on inbound calls, the person your agent called on outbound calls). Transferred calls are then carried and billed on your Twilio account. Admin or owner only.

cURL
curl -X POST https://api.thunderphone.com/v1/voip-connections/5/enable-transfers \
  -H "Authorization: Bearer sk_live_YOUR_API_KEY"

Returns 200 with the connection object: transfer_enabled_at set and transfer_refused_at cleared. It is safe to call again: Twilio is left unchanged if transfers are already on, and the call returns 200.

StatusWhen
409Not a Twilio connection (turn on call transfers, SIP REFER, in your carrier's settings; detail says where), the connection has no SIP trunk or credentials, or Twilio has no such trunk
502Twilio rejected the stored credentials or the update failed

Test credentials

Probes the provider API with the supplied credentials (for manual, checks that the SIP host resolves to a public address and, for TCP/TLS, accepts a connection on the port — a real call is only placed when a number is verified). On success, the response includes a verification_evidence_id you can pass to Create or Update to create/rotate the connection without a second round trip.

Pass connection_id to test an edit: the submitted credentials / sip_config are merged over the stored connection (masked, blank or omitted secrets keep their stored values, exactly as Update will merge them), so the evidence matches the update you are about to send.

cURL
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 (success)
{
  "status": "pass",
  "verification_evidence_id": "b9a2...",
  "suggested_connection_name": "Telnyx: Acme Main (+15550001234)",
  "checks": {
    "credentials_valid": true,
    "inbound_reachable": true,
    "outbound_authorized": true
  }
}

Get the inbound SIP host

GET /v1/voip-connections/sip-host

Returns the SIP host a manual connection's trunk must send inbound calls to: sip.thunderphone.com, ThunderPhone's SIP edge. Point the trunk at <sip_host>:5060 for UDP or TCP, or <sip_host>:5061 for TLS, and address each call to the dialed number in E.164 form, with the same number, including the +, in both the request line and the To header (sip:+14155550142@<sip_host>). Read it from here rather than keeping a copy. Trunks set up earlier with a <your-project>.sip.livekit.cloud address keep working while the connection has no allowed source IPs. The edge's IP addresses are listed under IP allowlisting.

cURL
curl https://api.thunderphone.com/v1/voip-connections/sip-host \
  -H "Authorization: Bearer sk_live_YOUR_API_KEY"
Response
{
  "sip_host": "sip.thunderphone.com"
}
FieldTypeDescription
sip_hoststring | nullHostname only, without scheme or port. null when inbound SIP is not configured in this environment; contact support

Suggest a connection name

Asks the provider for an inferred display name. Used by the dashboard onboarding flow.

cURL
curl -X POST https://api.thunderphone.com/v1/voip-connections/suggest-name \
  -H "Authorization: Bearer sk_live_YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "provider":    "telnyx",
    "credentials": { "apiKey": "KEY..." }
  }'
Response
{
  "suggested_connection_name": "Telnyx: Acme Main",
  "suggested_connection_name_inferred": true
}

Discover Twilio subaccounts

Lists the active subaccounts a Twilio parent account can see, marking the ones this org already has a connection for. Pass either raw parent credentials or the connection_id of an existing Twilio connection. Subaccount auth tokens are read from Twilio and kept server-side under the returned discovery_id (valid for 30 minutes); they are never returned. Admin role required.

cURL
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 }
  ]
}

An empty subaccounts array means the credentials belong to an account with no active subaccounts (or to a subaccount itself).


Connect Twilio subaccounts

Creates one ordinary Twilio connection per selected subaccount, each with its own verification evidence and SIP trunk inside that subaccount. Rows are independent: one subaccount failing never blocks the others, and a failed sid can be sent again with the same discovery_id until it expires. Admin role required.

cURL
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...", "ACsales..."]
  }'
Response
{
  "results": [
    { "sid": "ACsupport...", "friendly_name": "Acme Support", "status": "created", "connection": { "id": 9, "provider": "twilio", "setup_method": "twilio_subaccount", "...": "..." }, "error": null },
    { "sid": "ACsales...",   "friendly_name": "Acme Sales",   "status": "skipped", "connection": { "id": 5, "...": "..." }, "error": null }
  ]
}
statusMeaning
createdA new connection (and trunk) exists; connection is the new Connection object
skippedThe org already had a connection for this subaccount; connection is that one
failedNothing was created; error says why (Twilio rejected the token, trunk creation failed, unknown sid)

Returns 400 when the discovery_id has expired ("Subaccount discovery expired. Run discovery again.").


List available numbers

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

Returns the list of phone numbers visible to this connection's credentials that are not yet imported into any ThunderPhone org:

[
  {
    "id": "provider-handle-abc",
    "number": "+15550001234",
    "friendlyName": "Main office",
    "type": "local",
    "capabilities": ["voice"],
    "region": "US-CA",
    "monthlyFee": 100
  }
]

Import numbers

Moves the selected numbers into ThunderPhone as Phone number resources. Imports start in status="provisioning" — call POST /v1/phone-numbers/{id}/verify-voip to complete verification. For a manual SIP connection that places a short probe call from the number, through your trunk, to a ThunderPhone number — see Verify a VoIP-sourced number.

cURL
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"]}'
Response
{
  "imported_count": 2,
  "provider_charge_estimate_cents": 0,
  "provider_charge_note": "Import does not create ThunderPhone charges. Provider charges are billed by your VoIP provider only.",
  "results": [
    { "id": 201, "number": "+15550001234", "status": "provisioning" },
    { "id": 202, "number": "+15550009999", "status": "provisioning" }
  ]
}

numbers accepts 1–50 entries per request.


Search purchasable numbers

For connections where provisioning_supported is true (currently Telnyx only), search the provider's inventory for numbers you can buy without leaving ThunderPhone.

cURL
curl 'https://api.thunderphone.com/v1/voip-connections/5/search-numbers?country=US&area_code=415&type=local' \
  -H "Authorization: Bearer sk_live_YOUR_API_KEY"
Query paramRequiredDescription
countryyesTwo-letter ISO country code
typeyeslocal or toll-free
area_codenoDigits only
Response
{
  "results": [
    {
      "id": "provider-handle-abc",
      "number": "+14155550123",
      "friendlyName": "+1 415 555 0123",
      "type": "local",
      "capabilities": ["voice"],
      "region": "US-CA",
      "monthlyFee": 100,
      "monthlyFeeCurrency": "USD"
    }
  ],
  "provider_charge_note": "Purchasing this number and its recurring monthly service are billed directly by Telnyx. ThunderPhone does not add a provisioning fee."
}
StatusCondition
200Search results
501Provider does not support guided provisioning — use Import
502Provider API error / temporarily unavailable

Provision (buy) a number

Places a number order with the provider using the connection's credentials, then registers the number in ThunderPhone. The provider — not ThunderPhone — bills the purchase and monthly fee.

cURL
curl -X POST https://api.thunderphone.com/v1/voip-connections/5/provision-number \
  -H "Authorization: Bearer sk_live_YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"number": "+14155550123"}'
FieldTypeRequiredDescription
numberstringyesExplicit E.164 number from a search result
Response (201 Created)
{
  "order_id": "ord_abc123",
  "order_status": "pending",
  "provider_charge_note": "Purchasing this number and its recurring monthly service are billed directly by Telnyx. ThunderPhone does not add a provisioning fee.",
  "result": { "id": 203, "number": "+14155550123", "status": "provisioning" }
}

Returns 201 Created when a new order was placed, or 200 OK when the number was already owned and only registration was needed.

StatusCondition
400Invalid number / order rejected
501Provider does not support guided provisioning
502Provider API error / temporarily unavailable