> ## Documentation Index
> Fetch the complete documentation index at: https://docs.orbit.devotel.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Connect a SIP trunk (BYOC): bring your own carrier or PBX to Orbit

> Connect your own SBC, PBX, or SIP carrier to Orbit as a trunk — inbound and outgoing directions, IP allowlisting and digest auth, routing rules, failover, health checks, and troubleshooting registration, RTP, and codec problems.

# Connect a SIP trunk (BYOC)

Orbit supports **BYOC** — bring your own carrier, PBX, or session
border controller — through SIP trunks. A trunk connects your own
voice equipment or carrier relationship to Orbit while Orbit handles
flows, IVRs, queues, dialers, agents, and billing. You keep the
provider you already have (or the PBX you already run) and route its
calls through Orbit's platform.

Trunks come in two directions, and you pick one when you create the
trunk:

* **Outgoing** — Orbit dials *out* to your PBX/SBC over SIP to deliver
  calls into your equipment (for example, an inbound flow that ends in
  a SIP transfer to your PBX).
* **Incoming** — Your PBX or carrier originates calls *into* Orbit
  over SIP, where they enter flows, IVRs, queues, or an AI voice
  agent. This is the classic BYOC pattern: your carrier terminates to
  you, Orbit gives it the platform.

## Prerequisites

Before you create a trunk you need:

1. **SIP credentials or endpoint details from your carrier/PBX.** For
   an outgoing trunk: the hostname (or IP), port, transport
   (UDP/TCP/TLS), and optional digest username and password Orbit
   should use when registering or sending an INVITE to your side. For
   an incoming trunk: whether your carrier authenticates by source IP,
   by SIP digest username/password, or by both.
2. **Static source IPs you control.** For an incoming trunk with IP
   allowlisting, you must know the exact static egress IPs of your
   SBC/carrier — a NAT whose egress rotates will drop calls the moment
   the address changes. The same IP-hygiene discipline in
   [API key IP allowlisting](/guides/api-key-ip-allowlist) applies
   here: allowlist exact IPs or tight CIDRs, never `0.0.0.0/0`.
3. **An admin-role API key** if you prefer the API over the dashboard
   (writes to trunk endpoints are admin-guarded; reads work with a
   standard read key).
4. **A routing target for inbound calls** — a flow, IVR, or queue to
   send incoming-trunk calls into. Ringing a trunk without a target
   just ends calls.

## Create a trunk in the dashboard

Open **Voice → SIP trunks** in the dashboard. The page has tabs for
**Outgoing**, **Incoming**, **Routing**, and **Usage**.

For an outgoing trunk, click **Add trunk**, enter the host, port,
transport, and (optionally) your digest credentials, then run the
pre-save probe the dialog offers — the probe issues a real REGISTER
against your endpoint and carries the verdict onto the new trunk row,
so you know at creation time whether Orbit can reach your PBX, not
after traffic is already failing.

For an incoming trunk, choose the **Incoming** tab and pick an auth
mode — `ip_allowlist`, `digest`, or `both` (digest required ON TOP of
the IP check). IP-only is convenient but weaker; for a shared SBC that
other customers of your carrier also share, prefer `both`.

## Create a trunk via the API

Both directions are also creatable programmatically. All endpoints sit
under `/api/v1/voice/sip-trunks`.

Outgoing (Orbit → your PBX):

```bash theme={null}
curl -X POST https://api.orbit.devotel.io/api/v1/voice/sip-trunks \
  -H "X-API-Key: dv_live_sk_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Office PBX — Dallas",
    "host": "pbx.example.com",
    "port": 5060,
    "transport": "tls",
    "authentication": {
      "username": "orbit",
      "password": "s3cret"
    },
    "codecs": ["PCMU", "PCMA"],
    "maxConcurrent": 40,
    "enabled": true
  }'
```

| Field             | Notes                                                                                                                                 |
| ----------------- | ------------------------------------------------------------------------------------------------------------------------------------- |
| `host`            | Hostname or IP of your PBX/SBC. Must be publicly resolvable — the create handler returns a 422 with a DNS-resolution error otherwise. |
| `port`            | Defaults to 5060.                                                                                                                     |
| `transport`       | `udp` (default), `tcp`, or `tls`. Prefer `tls` end-to-end.                                                                            |
| `authentication`  | Optional digest credentials Orbit presents when your side challenges.                                                                 |
| `codecs`          | Up to 20 codec names in preference order (e.g. `PCMU`, `PCMA`, `OPUS`).                                                               |
| `maxConcurrent`   | Per-trunk concurrent-call ceiling, up to 2000.                                                                                        |
| `failoverTrunkId` | Optional — another trunk's `id` (starts with `sip_`) to walk to when this trunk is unregistered at dispatch.                          |

Incoming (your PBX/carrier → Orbit):

```bash theme={null}
curl -X POST https://api.orbit.devotel.io/api/v1/voice/sip-trunks/incoming \
  -H "X-API-Key: dv_live_sk_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Carrier BYOC — inbound",
    "authMode": "both",
    "allowedIps": ["203.0.113.0/24"],
    "allowedCallerIds": ["+14155550010"],
    "perTrunkLimits": {
      "maxConcurrent": 30,
      "dailySpendCapCents": 200000
    },
    "enabled": true
  }'
```

| Field              | Notes                                                                                                                             |
| ------------------ | --------------------------------------------------------------------------------------------------------------------------------- |
| `authMode`         | `ip_allowlist`, `digest`, or `both`. `both` requires the IP check to pass AND a valid SIP digest.                                 |
| `allowedIps`       | Up to 50 CIDRs or exact IPs you originate from. Tight values only.                                                                |
| `allowedCallerIds` | Optional E.164 caller IDs you accept on this trunk, up to 100. When set, an inbound INVITE whose caller isn't listed is rejected. |
| `perTrunkLimits`   | Optional guardrails: `maxConcurrent`, `dailySpendCapCents`, `hourlyCallCap`.                                                      |
| `routingRules`     | Optional per-trunk rules — destination prefix/country blocklists, caller-ID rewrites, time windows, and a CDR webhook URL.        |

Both responses return the full trunk object with its generated `id`
(`sip_...`). For a digest-authenticated incoming trunk, the response
also carries the digest credentials — save the password immediately:
it is only shown at creation and is not recoverable afterwards
(rotation issues a new one via
`POST /api/v1/voice/sip-trunks/{id}/credentials/rotate`, followed by
a single-use reveal on
`GET /api/v1/voice/sip-trunks/{id}/credentials/reveal`).

Update either direction with `PUT /api/v1/voice/sip-trunks/{id}` —
partial bodies only change the fields you send.

<Note>Outbound (MT) calls to the PSTN always egress through Orbit /
Devotel's wholesale softswitch — never through your trunk to a
third-party provider. Trunks connect your equipment to Orbit; they
never take over the platform's outbound PSTN exit.</Note>

## Inbound routing: attach the trunk to a flow

An incoming trunk on its own answers nothing — its traffic enters
Orbit but has no target until something routes it. The routing target
is your normal inbound surface:

* **Flows / IVRs** — point the inbound route for a caller-id or DID on
  this trunk into a flow. The [Build an IVR flow](/guides/build-ivr-flow)
  guide covers the IVR itself; the trunk simply feeds calls into it.
* **Queues** — route calls into a voice queue so they land on agents
  ([Voice queues](/guides/voice-queues)).
* **AI voice agent** — route the call into your agent with the
  `sip_forward` action ([AI voice agents via sip\_forward](/guides/ai-voice-agent-sip-forward)).

The association lives on the inbound route (DID/caller-id → flow,
queue, or agent), not on the trunk itself — the trunk controls
authentication and guardrails; the route controls the destination.

## Outbound routing: caller IDs and the dialer

Publish the numbers you want to show as caller IDs, then use them from
surfaces that place outbound voice:

* **Dialer campaigns** reference the caller ID you assign to the
  campaign — see [Launch an outbound dialer campaign](/guides/outbound-dialer-campaign).
* **Manual/agent calls** use the caller ID configured on the Voice
  side of the dashboard.

Remember the trunk's role here: the dialer's PSTN exit stays on the
platform (per the note above). An outgoing trunk is about delivering
calls into YOUR equipment — a flow that SIP-transfers to your PBX, for
example — not about moving the dialer off Orbit's wholesale path.

## Health checks and failover

Orbit keeps a live picture of every trunk.

* **Registration status (outgoing).** A reconciler re-polls REGISTER
  continuously and the dashboard's **Test trunk** button runs the same
  probe on demand
  (`POST /api/v1/voice/sip-trunks/{id}/test`). The trunk row and the
  API expose `status`, `lastRegisteredAt`, `lastRegistrationLatencyMs`,
  and `lastRegistrationError` so a failing registration surfaces with
  a reason, not a bare "unregistered".
* **Accepted-call freshness (incoming).** An incoming trunk has no
  REGISTER (your side initiates), so it tracks accepted traffic:
  accepted within 5 minutes → "registered", within an hour →
  "trying", otherwise "idle". Rejections surface on `lastCallVerdict`
  with the exact reason (auth failure, caller-id mismatch, routing
  rule violation, billing hold).
* **Fleet snapshot.** `GET /api/v1/voice/sip-trunks/health` returns an
  attention-ranked snapshot across all trunks — use it for an ops
  dashboard or an alerting hook. `GET /api/v1/voice/sip-trunks/route-quality`
  gives per-trunk ASR/SDR scoring and capacity bands for the outbound
  side. See also [Number health](/numbers/health) for per-number
  warming and deliverability; the trunk health snapshot sits next to
  it in overall fleet observability.

**Failover.** Set `failoverTrunkId` on an outgoing trunk to point at
another trunk (outgoing OR incoming — an incoming trunk's first
allowed IP is your own already-validated SBC address, so it makes a
valid fallback). When a dispatch finds the primary trunk unregistered,
Orbit walks the failover chain first, then falls back to the platform
default, keeping the call down instead of dropping it. Chains are
cycle-checked — an update that would create a loop is rejected with a
422\.

## Troubleshooting

### Registration failures (outgoing trunks)

1. Read the reason on the trunk row: `GET` the trunk and look at
   `lastRegistrationError` — the probe and reconciler write the
   sanitized registrar response ("Registrar rejected the credentials…",
   DNS resolution failure, timeout) there, no log dive needed.
2. Verify reachability: your firewall must allow Orbit's UDP/TCP/TLS
   connect on the port you set, and your SBC must answer REGISTER with
   the credentials you configured. A wrong port or a blocked transport
   looks identical to "not responding" — confirm both ends.
3. If the dashboard shows "unregistered" right after creation, run
   **Test trunk** — the probe also refreshes the row's status if
   you've already fixed the far end.

### RTP / media issues (one-way audio, no audio)

* Confirm both directions of media are allowed through your SBC:
  signalling alone succeeding means the call sets up but stays silent.
  Whitelist Orbit's media IPs, not just the signalling edge.
* Check NAT: your PBX must advertise a routable IP in SDP; an
  internal/private SDP address gives one-way or no audio. Enable your
  SBC's NAT-traversal or host the PBX behind a proper SBC.
* Timeouts on unanswered calls with no media are usually a firewall
  that accepts SIP but drops RTP — allow the media port range, not
  only 5060/5061.

### Codec mismatches

Symptoms: calls connect and immediately hang up, or one leg hears
garbled audio.

* Set an explicit `codecs` list on the trunk in preference order —
  PCMU/PCMA for PSTN-compatible endpoints, OPUS only where both sides
  genuinely support it.
* If your PBX negotiates OPUS but the other side (a PSTN carrier)
  doesn't, the call fails — narrow the list to codecs both endpoints
  offer.

### Incoming trunk rejections

Read `lastCallVerdictReason` on the trunk:

* **Auth failure** — the source IP isn't in `allowedIps` (or the
  digest failed when `authMode: both` or `digest`). Confirm your
  actual egress IP — a NAT or carrier that rotates will reject
  intermittently.
* **Caller-id mismatch** — you set `allowedCallerIds` and the caller ID
  on the INVITE wasn't in it; widen the list or remove it.
* **Routing rule violation** — a per-trunk `routingRules` blocklist or
  time-window rejected the call; inspect the rules you set.
* **Billing hold** — the per-trunk cap (`maxConcurrent`,
  `dailySpendCapCents`, `hourlyCallCap`) tripped; raise the limit or
  wait for the period to roll.

## Endpoints

| Method   | Path                                               | Purpose                                              |
| -------- | -------------------------------------------------- | ---------------------------------------------------- |
| `GET`    | `/api/v1/voice/sip-trunks`                         | List all trunks (both directions)                    |
| `POST`   | `/api/v1/voice/sip-trunks`                         | Create an outgoing trunk                             |
| `POST`   | `/api/v1/voice/sip-trunks/incoming`                | Create an incoming trunk                             |
| `GET`    | `/api/v1/voice/sip-trunks/{id}`                    | Get one trunk                                        |
| `PUT`    | `/api/v1/voice/sip-trunks/{id}`                    | Update a trunk                                       |
| `DELETE` | `/api/v1/voice/sip-trunks/{id}`                    | Delete a trunk                                       |
| `POST`   | `/api/v1/voice/sip-trunks/probe`                   | Pre-save connectivity probe                          |
| `POST`   | `/api/v1/voice/sip-trunks/{id}/test`               | Run an on-demand REGISTER probe                      |
| `POST`   | `/api/v1/voice/sip-trunks/{id}/credentials/rotate` | Rotate digest credentials (incoming)                 |
| `GET`    | `/api/v1/voice/sip-trunks/{id}/credentials/reveal` | One-time reveal of a freshly-rotated digest password |
| `GET`    | `/api/v1/voice/sip-trunks/health`                  | Attention-ranked health snapshot across trunks       |
| `GET`    | `/api/v1/voice/sip-trunks/route-quality`           | Per-trunk ASR/SDR + capacity bands                   |
| `GET`    | `/api/v1/voice/sip-trunks/{id}/usage`              | Per-trunk usage metrics                              |
| `GET`    | `/api/v1/voice/sip-trunks/{id}/utilization`        | Live concurrent-call utilization                     |
| `GET`    | `/api/v1/voice/sip-trunks/{id}/routing-analysis`   | Per-trunk routing/analysis hint for a destination    |

## See also

* [Connect via SMPP](/guides/smpp) — the SMS analog of a BYO carrier:
  bind your own SMPP client to Orbit the same way a SIP trunk brings
  your own voice carrier to Orbit
* [AI voice agents via sip\_forward](/guides/ai-voice-agent-sip-forward) —
  SIP-forwarding inbound calls into a third-party AI agent
* [Build an IVR flow](/guides/build-ivr-flow) — the inbound routing
  target you're attaching the trunk to
* [Voice queues](/guides/voice-queues) — queue-based inbound routing
* [Number health](/numbers/health) — per-number deliverability and
  warming, alongside trunk-level health
* [Voice](/api-reference/voice) — API reference for the voice surface
