---
title: "Bring your own numbers (VoIP, API)"
description: "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**.

<Note>
  The dashboard has a guided version of this whole flow under
  **Connections → VoIP** (`/dashboard/voip-connections`): a
  connect-existing path first (Twilio and Telnyx by API key; SignalWire,
  Vonage, Plivo and any other trunk through the manual SIP form with
  provider-specific steps and an AI setup-guide generator), and a
  guided setup second that opens a new Telnyx account.
</Note>

## Supported providers

| Provider | `provider` id | Notes |
|----------|---------------|-------|
| Twilio | `twilio` | Account SID + Auth Token; one connection per Twilio account or subaccount (see [Twilio subaccounts](#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.

```bash
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" }
  }'
```

```json 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:

```bash
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](/api-reference/voip-connections#connection-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:

```bash
# 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": "..."}}'
```

```json 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 }
  ]
}
```

```bash
# 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:

```bash
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:

```bash
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](/api-reference/phone-numbers)
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.

```bash
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](/guides/handle-inbound-calls) and
[Place outbound calls](/guides/place-outbound-calls).

## Rotating credentials

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

```bash
# 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

<CardGroup cols={2}>
  <Card title="VoIP connections reference" icon="phone-volume" href="/api-reference/voip-connections">
    Every field on the connection, evidence, and import responses.
  </Card>
  <Card title="Phone numbers reference" icon="phone" href="/api-reference/phone-numbers">
    Assign agents, transfer between orgs, release numbers.
  </Card>
  <Card title="Place outbound calls" icon="arrow-up-right" href="/guides/place-outbound-calls">
    Now that you own the number, start placing outbound.
  </Card>
</CardGroup>
