Bring your own carrier (BYOC)
Use your own carrier's numbers and PSTN connectivity with Contactivity trunks and voice apps. Your carrier sends calls for its numbers to Contactivity; calls placed with one of its numbers as caller ID leave through it. You pay your carrier for the PSTN and Contactivity a platform fee per minute.
Paths are relative to https://api.contactivity.io/v1/projects/{project_id}; see the overview.
Creating or changing carriers and numbers needs account tier 2 or higher (403 tier_required).
| Method | Path | Scope | Purpose |
|---|---|---|---|
| GET | /byoc/carriers |
byoc:read |
List carriers (paged) |
| POST | /byoc/carriers |
byoc:write |
Create a carrier |
| GET | /byoc/carriers/{carrier_id} |
byoc:read |
Fetch a carrier |
| PATCH | /byoc/carriers/{carrier_id} |
byoc:write |
Update a carrier |
| DELETE | /byoc/carriers/{carrier_id} |
byoc:write |
Archive a carrier |
| POST | /byoc/carriers/{carrier_id}/emergency |
byoc:write |
Choose how 911 calls are routed |
| GET | /byoc/carriers/{carrier_id}/numbers |
byoc:read |
List the carrier's numbers (paged) |
| POST | /byoc/carriers/{carrier_id}/numbers |
byoc:write |
Add a number |
| GET | /byoc/carriers/{carrier_id}/numbers/{phone_number} |
byoc:read |
Fetch a number |
| PATCH | /byoc/carriers/{carrier_id}/numbers/{phone_number} |
byoc:write |
Route a number |
| DELETE | /byoc/carriers/{carrier_id}/numbers/{phone_number} |
byoc:write |
Remove a number |
How it works
- A carrier belongs to one project. It has outbound gateways (where Contactivity sends calls) and inbound source IPs (where its calls come from).
- Inbound: a call from one of the carrier's source IPs to one of its numbers goes to that number's route, like a
number bought from Contactivity. A number routed to
forwardis forwarded out through the same carrier. Calls from those IPs to other numbers are not accepted as carrier calls. - Outbound: a call from your trunks, voice apps or the Calls API whose caller ID is one of the carrier's numbers leaves through the carrier's gateways. Any other caller ID uses Contactivity's own carriers.
- Numbers bought from Contactivity never use BYOC, in either direction, and cannot be added to a carrier.
- The same number can be on carriers of different organizations: an inbound call is matched by source IP and
number together. If both match one call, it is refused (
403). - Contactivity does not sign outbound BYOC calls with STIR/SHAKEN; your carrier attests them.
The carrier object
| Field | Type | Description |
|---|---|---|
id |
string | byoc_… |
project_id |
string | |
name |
string | |
status |
string | active or archived |
gateways |
object[] | Outbound gateways in failover order: id, host, port, transport, priority, username, has_password |
source_ips |
string[] | Addresses (203.0.113.7) and ranges (198.51.100.0/28) your carrier sends calls from |
emergency |
object | How 911 calls are routed; see Emergency calls |
numbers_count |
integer | Active numbers on the carrier |
created_at |
timestamp | |
archived_at |
timestamp or null |
Create or update a carrier
| Field | Type | Rules |
|---|---|---|
name |
string | Required on create. 1-100 characters, trimmed |
gateways |
object[] | Required on create. 1-10 gateways |
source_ips |
string[] | Required on create. 1-50 public IPv4 addresses or ranges of /24 or narrower |
A gateway:
| Field | Type | Rules |
|---|---|---|
host |
string | Required. A public IPv4 address or hostname, without a port |
port |
integer | 1-65535. Default 5060 (5061 for tls) |
transport |
string | udp (default), tcp or tls |
priority |
integer | 0-100, lower first; default 0 |
username |
string | Optional digest login, if your carrier challenges INVITEs |
password |
string | At most 128 characters, needs username. Never returned (has_password says if one is set) |
PATCH changes only the fields you send; gateways and source_ips are replaced as a whole. A gateway sent
without password keeps the stored password of the gateway with the same host, port, transport and username.
curl https://api.contactivity.io/v1/projects/$PROJECT_ID/byoc/carriers \
-H "Authorization: Bearer $CONTACTIVITY_API_KEY" \
-H "Content-Type: application/json" -H "Idempotency-Key: $(uuidgen)" \
-d '{
"name": "Acme Telecom",
"gateways": [
{"host": "sip1.acme.example", "port": 5060, "transport": "udp", "priority": 0},
{"host": "sip2.acme.example", "port": 5060, "transport": "udp", "priority": 1}
],
"source_ips": ["198.51.100.0/28", "203.0.113.7"]
}'Errors: 400 invalid_parameter (param name, gateways, gateways[i].host, gateways[i].port,
gateways[i].transport, gateways[i].priority, gateways[i].username, gateways[i].password, source_ips),
400 invalid_request (a source range that overlaps a SIP trunk's allowed IPs), 400 unknown_parameter,
403 tier_required, 404 not_found, 409 archived.
DELETE archives the carrier and its numbers: its gateways and source IPs stop working at once.
Gateways and failover
An outbound call tries the gateway with the lowest priority first (equal priorities: the order sent). When a
gateway does not answer, answers 408 or 5xx, or asks for a login that it does not accept, the call moves to the
next one. Any other answer (busy, not found, declined) ends the call: it is the callee's answer, not a gateway
failure. When every gateway fails, the caller gets 503.
Source IPs
Ask your carrier for the addresses it sends calls from. A range alone opens nothing: only calls to the carrier's numbers are accepted from it. A range that overlaps a SIP trunk's allowed IPs is refused, because those addresses place calls.
Numbers
Add your carrier's numbers to the carrier. There is no verification: you confirm you hold them.
| Field | Type | Rules |
|---|---|---|
phone_number |
string | Required on add. E.164 |
route_id |
string or null | A route of the project (routes). null unroutes |
fallback |
boolean | Default false. true: a call that fails on the route goes on to the route's fallback route |
The number object: id and phone_number (the E.164 number), carrier_id, project_id, route_id, fallback,
created_at, archived_at. PATCH takes route_id and/or fallback; fallback alone keeps the route. Phone numbers in paths are URL-encoded (/numbers/%2B14155550123).
curl https://api.contactivity.io/v1/projects/$PROJECT_ID/byoc/carriers/$CARRIER_ID/numbers \
-H "Authorization: Bearer $CONTACTIVITY_API_KEY" \
-H "Content-Type: application/json" -H "Idempotency-Key: $(uuidgen)" \
-d '{"phone_number": "+14155550123", "route_id": "rt_us1_…"}'Errors on add: 400 invalid_parameter (param phone_number, route_id), 403 tier_required, 404 not_found,
409 number_in_inventory (a Contactivity number: route it under numbers instead),
409 already_exists (already on a carrier of your organization), 409 archived (the carrier).
On PATCH: 400 missing_parameter and 400 invalid_parameter (param route_id), 404 not_found, 409 archived.
DELETE archives the number (204).
A BYOC number can be the caller ID of calls from your trunks and voice apps; those calls leave through its
carrier. It is not listed under /numbers.
Emergency calls
A 911 or 933 call whose caller ID, or the calling trunk's default_caller_id, is one of the carrier's numbers
is not sent to your carrier until you consent to route it there. With consent, the call goes to the carrier's
gateways. Read emergency on the carrier:
| Field | Description |
|---|---|
mode |
carrier, platform (consent withdrawn), or null (never chosen) |
route |
carrier when consent is recorded, else platform |
consent_at, consent_by, consent_text_version |
The recorded consent, or null |
POST /byoc/carriers/{carrier_id}/emergency:
| Field | Type | Rules |
|---|---|---|
mode |
string | Required. carrier (give consent) or platform (withdraw it) |
consent |
boolean | Required true with carrier: you confirm your carrier delivers 911 calls for these numbers, with the caller's location. Not allowed with platform |
consent_text_version |
string | Required with carrier, 1-50 characters: the version of the consent text the person accepted |
curl https://api.contactivity.io/v1/projects/$PROJECT_ID/byoc/carriers/$CARRIER_ID/emergency \
-H "Authorization: Bearer $CONTACTIVITY_API_KEY" -H "Content-Type: application/json" \
-d '{"mode": "carrier", "consent": true, "consent_text_version": "2026-10"}'Who consented, when and from which address is kept in the project's audit log. {"mode": "platform"} clears
the consent. Returns the carrier. Errors: 400 consent_required, 400 invalid_parameter (param mode,
consent, consent_text_version), 403 tier_required, 404 not_found, 409 archived.
Billing
BYOC legs are billed a platform fee per minute, the same inbound and outbound and for every destination; no PSTN
rate applies. The product is byoc_sip_trunking for legs to or from your trunks and byoc_programmable_voice for
legs handled by a voice app. Check a fee with GET /pricing/check?product=byoc_programmable_voice
(Voice apps); legs show their product in calls and
call records.
Limits
Calls through your carrier count toward the organization's calls-per-second and concurrent-call limits
(GET /limits).

