# 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](/guides/overview.md).
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 `forward` is 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](#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.

```sh
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](/guides/trunks-routes.md)). `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`).

```sh
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](/guides/numbers.md) 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 |

```sh
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.md](/guides/voice-apps.md#pricing)); legs show their product in [calls](/guides/calls.md) and
[call records](/guides/billing.md#call-detail-records).

## Limits

Calls through your carrier count toward the organization's calls-per-second and concurrent-call limits
(`GET /limits`).
