> ## 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 end to end: register, route, test, monitor

> Provision an outgoing or incoming SIP trunk, validate it before save, grab the one-time credentials, route calls onto a DID, run a live test call, and monitor registration health and route quality.

# Connect a SIP trunk

A SIP trunk connects your own phone system to Orbit. This guide walks the
full lifecycle: decide the direction, create the trunk, validate and test
it, wire it to a DID, then keep an eye on registration and route quality.
Everything here also exists in the dashboard under **Settings → SIP trunks**.

## Step 1 — Pick the direction

Orbit supports two trunk directions:

| Direction    | Who originates calls                | Use it for                                                                                                              |
| ------------ | ----------------------------------- | ----------------------------------------------------------------------------------------------------------------------- |
| **Outgoing** | Orbit dials out to **your carrier** | Keep your own PSTN carrier for outbound dialing — bring-your-own-carrier                                                |
| **Incoming** | **Your PBX** dials into Orbit       | Connect an office PBX (Asterisk, FreeSWITCH, 3CX) so phones on the PBX can use Orbit numbers, flows, agents, and queues |

Incoming trunks authenticate your PBX on every call, by source IP, digest
credentials, or both. Outgoing trunks carry your carrier's SIP host and the
register credentials the carrier issued you.

**Prerequisites**

* For outgoing trunks: the SIP host, port, and (if your carrier registered
  your account) username/password from your carrier. For US outbound
  traffic, Orbit applies STIR/SHAKEN attestation to the caller ID — see
  [STIR/SHAKEN](/channels/voice/stir-shaken).
* For incoming trunks: your PBX must be able to send SIP INVITEs to a
  public domain over UDP 5060, TCP 5060, or TLS 5061, and either present a
  stable source IP or answer digest challenges.
* To place calls, own at least one DID — buy one under
  **Numbers → Buy numbers** or via the Numbers API.

## Step 2 — Create an outgoing trunk

Outgoing trunks are the ones you use for dialing out through your own
carrier.

### Validate before you save

Probe first — the probe runs a real SIP REGISTER against the carrier host
and returns the exact step that failed (DNS resolution, TCP/TLS connect,
or the REGISTER itself) so you fix it before persisting anything:

```bash theme={null}
curl -X POST https://api.orbit.devotel.io/api/v1/voice/sip-trunks/probe \
  -H "X-API-Key: dv_live_sk_..." \
  -H "Content-Type: application/json" \
  -d '{
    "host": "sip.carrier.example.com",
    "port": 5060,
    "transport": "udp",
    "register_username": "your-carrier-username",
    "register_password": "your-carrier-password"
  }'
# => { "data": { "reachable": true, "status": "registered", ... } }
```

A `reachable: false` response names the phase that failed and gives a
sanitized operator message — the dashboard's **Add trunk** dialog runs this
same probe before it lets you save.

### Create the trunk

```bash theme={null}
curl -X POST https://api.orbit.devotel.io/api/v1/voice/sip-trunks \
  -H "X-API-Key: dv_live_sk_..." \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Carrier A",
    "host": "sip.carrier.example.com",
    "port": 5060,
    "transport": "udp",
    "authentication": {
      "username": "your-carrier-username",
      "password": "your-carrier-password"
    },
    "maxConcurrent": 50
  }'
# => 201 { "data": { "id": "sip_abc123", ... } }
```

Credentials are encrypted at rest. Optional fields: `codecs`,
`maxConcurrent` (per-trunk concurrent-call cap, 1–2000),
`failoverTrunkId` (another trunk Orbit falls over to when this one drops
registration), and `enabled` (default `true`). If you ran the probe first,
pass its verdict as `probe_status` (`registered` | `unregistered` |
`unknown`) so the row starts with an accurate badge instead of a guessed
one — the dashboard does this automatically.

## Step 3 — Create an incoming trunk and capture the secret

Incoming trunks let your PBX originate calls into 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_..." \
  -H "Content-Type: application/json" \
  -d '{
    "name": "HQ PBX",
    "authMode": "both",
    "allowedIps": ["203.0.113.10", "203.0.113.11"]
  }'
# => 201 { "data": { "id": "sip_def456", "digestUsername": "trunk_a1b2c3",
#                     "digestPassword": "ONE-TIME SECRET",
#                     "credentials_revealed_once": true, ... } }
```

`authMode` is one of:

* `digest` — the PBX answers digest challenges with the issued
  username/password (handles SBCs with unstable egress IPs).
* `ip_allowlist` — only INVITEs from `allowedIps` are accepted (no
  password exists; nothing can be rotated).
* `both` — the PBX must match the allowlist **and** answer digest.

A few hard guards apply on the allowlist:

* Each entry must be a valid IPv4/IPv6 address or CIDR, and at most 50
  entries per trunk.
* `0.0.0.0/0` and `::/0` are rejected — a catch-all would disable the
  allowlist. Use `authMode: "digest"` if you don't want IP filtering.
* `authMode: "ip_allowlist"` or `"both"` requires at least one entry.

### Capture the digest secret — it is shown exactly once

The create response contains the plaintext `digestPassword` **once**, with
`credentials_revealed_once: true`. Every read of the trunk afterwards shows
the password masked as `***`. Save the username and password into your PBX
immediately, or rotate later:

```bash theme={null}
# 1. Rotate — the response carries a single-use reveal token, no plaintext
curl -X POST https://api.orbit.devotel.io/api/v1/voice/sip-trunks/sip_def456/credentials/rotate \
  -H "X-API-Key: dv_live_sk_..."
# => { "data": { "reveal_token": "...", "reveal_url": "...", "reveal_expires_at": "..." } }

# 2. Reveal once — read-and-consume; a second fetch returns 410 GONE
curl "https://api.orbit.devotel.io/api/v1/voice/sip-trunks/sip_def456/credentials/reveal?token=REVEAL_TOKEN" \
  -H "X-API-Key: dv_live_sk_..."
# => { "data": { "digestUsername": "trunk_a1b2c3", "digestPassword": "new-plaintext", ... } }
```

The reveal token is single-use and expires after five minutes — a replay
returns `410 GONE`, and rotation only applies to incoming trunks with
digest auth. Calls already established under the old password keep
running; the new password applies from the next INVITE — update your PBX
before then to avoid a 401 burst.

### Verify go-live readiness before routing traffic

Before you point calls at the trunk, run the readiness check. It is a
configuration verdict, not a network probe — incoming trunks never send a
REGISTER (your PBX initiates INVITEs toward Orbit, so "registered" does
not apply the way it does to outgoing trunks). The check confirms every
auth gate your `authMode` requires is satisfiable:

* `acl_configured` — every `allowedIps` entry parses as a valid IP or
  CIDR (required when the auth mode is `ip_allowlist` or `both`).
* `credentials_present` — digest username and password exist (required
  when the auth mode is `digest` or `both`).
* `auth_path_ready` — all required gates pass together.

```bash theme={null}
curl -X POST https://api.orbit.devotel.io/api/v1/voice/sip-trunks/sip_def456/onboarding/verify \
  -H "X-API-Key: dv_live_sk_..."
# Ready:
#   { "data": { "trunk_id": "sip_def456", "status": "verified",
#               "onboarded_at": "2026-08-25T12:00:00.000Z",
#               "checklist": ["acl_configured", "credentials_present",
#                             "auth_path_ready"],
#               "issues": [], "verified_at": "..." } }
# Blocked:
#   { "data": { "trunk_id": "sip_def456", "status": "not_ready",
#               "onboarded_at": null,
#               "checklist": ["acl_configured"],
#               "issues": ["Digest auth is enabled but credentials are
#                           missing — rotate credentials to regenerate them"],
#               "verified_at": "..." } }
```

On the first clean pass the trunk is stamped `onboarded_at` — the
go-live marker the dashboard onboarding wizard closes on. A blocked run
returns HTTP 200 with `status: "not_ready"` and the exact gates still
failing in `issues`, and that blocker is written onto the trunk so the
dashboard health surfaces show it until you re-verify.

Two rules keep the stamp honest:

* **Every later trunk edit clears `onboarded_at`** — changed the
  allowlist or credentials after onboarding? Re-verify; the stamp can
  never describe a stale configuration.
* **Reset restarts the wizard** —
  `POST /api/v1/voice/sip-trunks/{id}/onboarding/reset` clears the stamp
  and checklist without touching your auth config, for when you want to
  re-run the flow after changing the ACL outside the wizard.

Both endpoints apply to incoming trunks only; on an outgoing trunk they
return `422`. Readiness past go-live (auth failures, rejected INVITEs)
is covered by the registration polling and health snapshot in step 8 —
verify only gates whether the configuration is complete enough to carry
traffic.

## Step 4 — TLS and SRTP posture

Your carrier-facing config and your softphone-facing config have two
different security baselines:

| Surface                                 | Signalling                                                               | Media                      | Where you set it                                               |
| --------------------------------------- | ------------------------------------------------------------------------ | -------------------------- | -------------------------------------------------------------- |
| Trunk (Orbit ↔ your carrier/PBX)        | UDP/TCP 5060 or TLS 5061, per trunk `transport`                          | RTP through the media edge | `transport: "tls"` on the outgoing trunk body                  |
| SIP credential (your softphone ↔ Orbit) | TLS on port 5061 is the recommended transport (TCP/UDP 5060 as fallback) | SRTP is **mandatory**      | The returned `sip_transport_recommended` and `sip_srtp` fields |

Prefer `tls` on trunks when your carrier offers it — the create handler's
pre-save probe runs against the transport you pick, so a TLS path that
cannot complete the handshake fails the probe and never becomes a broken
saved row. On the credential side, every issued credential reports
`sip_srtp: "Mandatory"` in its connection details; a softphone that
advertises RTP-only media will not establish audio.

## Step 5 — Point your PBX at Orbit

Configure your PBX with the digest `digestUsername` and password from the
step above and the SIP edge host shown in **Settings → SIP trunks** in
the dashboard. For `authMode: "ip_allowlist"` or `"both"`, list every IP
your SBC can originate from — a burst of unauthorized attempts shows up in
the auth-failures ledger (step 8).

## Step 5 — Wire routing to a DID

Calls arriving on an incoming trunk enter Orbit with a dialed number; the
DID's inbound route decides where the call lands. Set it per number:

```bash theme={null}
curl -X PUT "https://api.orbit.devotel.io/api/v1/numbers/+14155550123/routing" \
  -H "X-API-Key: dv_live_sk_..." \
  -H "Content-Type: application/json" \
  -d '{
    "type": "webhook",
    "config": { "url": "https://your-app.example.com/voice/answer" }
  }'
```

Pick a route type per DID: `webhook` (your HTTPS URL returns call
instructions), `agent` (an AI voice agent), `sip_forward` (send the call to
a SIP URI or back through a trunk), `queue`, `ivr`, `voicemail`,
`softphone_user`, or `decline`. See the
[inbound routing reference](/api-reference/numbers) for the full set and
config shapes.

Two of those route types deserve a second look in a trunk setup:

* **`sip_forward` to an external AI voice agent** — point the DID at a
  third-party AI provider's SIP endpoint (callers.ai, Retell, Vapi, or a
  self-hosted PBX). Orbit terminates the carrier media and re-INVITEs your
  provider over TLS, so an inbound-trunk call can land in an AI agent
  without you running any SIP infrastructure. Full walkthrough:
  [AI voice agents via sip\_forward](/guides/ai-voice-agent-sip-forward).
* **Inbound to registered softphones** — when the trunk carries calls you
  want to ring desk phones or softphones directly, bind a SIP credential
  per device (`/api/v1/sip-credentials`): each issued credential registers
  against the SIP edge (`sip_server` in the response) and is routed on
  inbound DIDs of type `softphone_register`. Options on the credential
  include a per-device source-IP list (`allowedIpCidrs`), the outbound
  caller-id mode, a daily spend cap, and busy/no-answer/offline forward
  targets. That is the full cycle for a device: claim the credential,
  restrict its source IPs, and feed it inbound calls via routing.

For incoming trunks, optional `routingRules` on the trunk itself apply
before the DID route — a `countryBlocklist` or `destinationPrefixBlocklist`
blocks matching destinations, and `callerIdRewrites` swap the presented
caller ID per destination country or prefix. Simulate the fixed evaluation
order against any destination before going live:

```bash theme={null}
curl "https://api.orbit.devotel.io/api/v1/voice/sip-trunks/sip_def456/routing-analysis?destination=%2B14155552671" \
  -H "X-API-Key: dv_live_sk_..."
# => { "data": { "trunkId": "sip_def456", "executionOrder": [...], "simulation": { ... } } }
```

`executionOrder` lists the routing controls in the order dispatch applies
them and which ones you have configured; `simulation` (when you pass a
`destination`) shows which control wins and which caller-ID rewrite
applies. The simulation is read-only — it mirrors dispatch exactly.

## Step 6 — Test the trunk

**Validate configuration or re-check registration** — `POST /:id/test`. On
outgoing trunks this runs the same REGISTER probe as step 2 and records
the verdict on the trunk (it also stamps `lastRegistrationCheckAt`, which
the dispatch gate consults). On incoming trunks it validates the saved
config (allowlist entries, digest presence) and returns
`status: "ok" | "misconfigured"` with an `issues` list:

```bash theme={null}
curl -X POST https://api.orbit.devotel.io/api/v1/voice/sip-trunks/sip_abc123/test \
  -H "X-API-Key: dv_live_sk_..."
# => { "data": { "status": "registered", "steps": [...], ... } }
```

The registration verdict is one of three values:

* `registered` — the last check completed a REGISTER successfully.
* `unregistered` — the check ran and the registrar rejected or timed out;
  read `lastRegistrationError` on the trunk for the recorded reason.
* `unknown` — no check has run yet, or the verdict was acknowledged as
  uncertain at creation.

**Place a real call through the trunk** — `POST /:id/test-call` dials a
destination over the trunk and records the billable result. The destination
must be an E.164 number the org owns or an Orbit platform test number;
emergency-shaped dials are rejected with `EMERGENCY_CALLING_NOT_SUPPORTED`:

```bash theme={null}
curl -X POST https://api.orbit.devotel.io/api/v1/voice/sip-trunks/sip_abc123/test-call \
  -H "X-API-Key: dv_live_sk_..." \
  -H "Content-Type: application/json" \
  -d '{ "to": "+14155550100" }'
```

## Fax over the trunk (T.38)

Inbound fax shares the same inbound edge as voice: a fax sent to a DID
arrives as a `fax.received` event and follows the DID's fax routing to a
webhook, email recipients, or the inbox. If you carry fax on the same
trunk, keep the codec list on the trunk compatible — T.38 negotiation is
driven by the carrier side, and a mismatched codec set can force
unreliable audio fax. See
[Inbound fax routing](/guides/inbound-fax-workflow) for the receive-to-delivery
lifecycle and the per-DID routing contract.

## Step 7 — Emergency dialing

Orbit voice does not support emergency dialing (911/112/999/…), and every
voice surface rejects emergency short codes. For US numbers, still register
a dispatchable E911 address per DID — it records the location a regulator
expects to find even though Orbit's posture is block-and-disclaim:

```bash theme={null}
curl -X PUT "https://api.orbit.devotel.io/api/v1/numbers/num_abc123/emergency-address" \
  -H "X-API-Key: dv_live_sk_..." \
  -H "Content-Type: application/json" \
  -d '{
    "caller_name": "Acme Inc",
    "street_line_1": "1 Market St",
    "city": "San Francisco",
    "region": "CA",
    "postal_code": "94105",
    "country": "US"
  }'
```

See [Emergency address](/numbers/emergency-address) for field validation
rules.

## Step 8 — Monitor

### Health snapshot

Ranked attention-first, with severity counts — the same numbers the **SIP
trunks** page badges:

```bash theme={null}
curl "https://api.orbit.devotel.io/api/v1/voice/sip-trunks/health" \
  -H "X-API-Key: dv_live_sk_..."
# => { "data": { "trunks": [ { "id": "sip_abc123", "status": "unregistered", ... } ], "counts": { ... } } }
```

### Route quality

ASR/SDR-style scoring plus capacity bands per outgoing trunk over a
`24h` or `7d` window — read this before blaming a carrier: a saturated
concurrent-call cap shows up as blocked, not failed:

```bash theme={null}
curl "https://api.orbit.devotel.io/api/v1/voice/sip-trunks/route-quality?window=24h" \
  -H "X-API-Key: dv_live_sk_..."
```

### Usage per trunk

Seven days of daily totals by default, or a 24-hour attempt/accepted/
rejected breakdown with the top rejection reason on incoming trunks:

```bash theme={null}
curl "https://api.orbit.devotel.io/api/v1/voice/sip-trunks/sip_def456/usage?window_hours=24" \
  -H "X-API-Key: dv_live_sk_..."
```

For incoming trunks, failed digest/allowlist attempts have their own
seven-day ledger — the first place to look when calls stop arriving:

```bash theme={null}
curl "https://api.orbit.devotel.io/api/v1/voice/sip-trunks/incoming/sip_def456/auth-failures" \
  -H "X-API-Key: dv_live_sk_..."
```

### What the platform does on its own

Every enabled outgoing trunk is re-registered on a cadence of roughly
five minutes: a real REGISTER with the stored credentials, with the
verdict persisted back onto the trunk. If a check streak of transient
register failures accumulates, the trunk is flagged rather than silently
retried forever. Meanwhile a **dispatch gate** runs on every outbound
call: a trunk whose cached registration verdict is anything other than
`registered` (or whose verdict is stale) stops being offered new calls,
and the call walks the trunk's failover chain — the backup trunk you set
via `failoverTrunkId` — landing on the Devotel default outbound path if
no healthy failover exists. Your calls keep completing; the trunk row
keeps the recorded error until you fix it.

## Production checklist

* [ ] Outgoing trunk probed before save; verdict carried onto the row via `probe_status`
* [ ] Incoming trunk `authMode` chosen; allowlist entries are specific IPs/CIDRs, never `0.0.0.0/0`
* [ ] Digest password captured at create (or rotated + revealed once) and stored in the PBX — the plaintext is never shown again
* [ ] PBX pointed at the SIP edge host from **Settings → SIP trunks** with the issued digest credentials
* [ ] DID inbound route set (`PUT /numbers/+E164/routing`) to a webhook, agent, queue, or SIP target
* [ ] `routing-analysis` simulation run on a few representative destinations before sending traffic
* [ ] `test` returns `registered` (outgoing) / `ok` (incoming); `test-call` completed media end to end
* [ ] Trunk `transport` matches your carrier's TLS offer, and softphone credentials show `sip_srtp: "Mandatory"` in every UA
* [ ] Any fax DIDs confirmed codec-compatible on the trunk; inbound fax routing validated with a test fax
* [ ] E911 dispatchable address registered per US DID
* [ ] `failoverTrunkId` set on trunks that carry traffic, so the dispatch gate hands calls to a backup when registration drops
* [ ] Health + route-quality surfaced in your monitoring (poll `.../health` and `.../route-quality?window=24h`)

## When something breaks

Work through
[Troubleshooting: SIP trunk down or calls failing](/channels/voice/sip-trunks-troubleshooting)
— the four most common recorded causes are DNS failing to resolve the
host, a firewall blocking the SIP port, digest username/password mismatch,
and codec/TLS mismatch on the REGISTER handshake. For a symptom-first
checklist see also [SIP trunk troubleshooting](/troubleshooting/sip-trunk).
