# Trunk configuration reference

> Every field on the trunk form: SIP carrier vs WhatsApp Business Calling transport, SBC reachability, regional SIP edge pinning, inbound routing precedence, and secret rotation.

The **Trunks** screen edits one `trunk` row per carrier connection. A
trunk is the thing TeleQuick places calls out of and receives calls
on. This page explains what each group of fields on that form controls,
and how the read-only panels on the screen (effective inbound route,
WhatsApp webhook coordinates) are derived.

The form validates on submit: messages stay hidden until the first
failed save attempt, then update live as you fix fields. Numeric fields
hold real numbers, so an emptied or garbled port is rejected rather
than sent to the engine as `0` or `NaN`.

## Transport: SIP carrier trunk vs WhatsApp Business Calling

The `transport` field picks which kind of carrier connection this trunk
is, and it changes which sections of the form exist.

| Transport | What it is | Media |
| --------- | ---------- | ----- |
| `sip` | A conventional SIP carrier / SBC trunk. | RTP, terminated per the media mode. |
| `whatsapp` | A Meta Business Calling trunk. | WebRTC (`mod_whatsapp` + `mod_webrtc`). |

For a WhatsApp trunk there is **no SBC, no registration and no RTP
configuration**. Inbound calls arrive over the hosted webhook rather
than as an INVITE from an SBC, so the SIP-only sections are hidden when
`transport` is `whatsapp`. The fields that remain required at creation
time are:

| Field | Meaning |
| ----- | ------- |
| `wa_phone_number_id` | Meta Graph phone number id. This is the routing key that maps an inbound WhatsApp call to this trunk. |
| `wa_waba_id` | WhatsApp Business Account id. |
| `wa_access_token` | Graph API access token. Secret. |
| `wa_app_secret` | Meta app secret, used to verify webhook payloads. Secret. |
| `wa_graph_version` | Graph API version pin, e.g. `v23.0`. Leave empty to use the engine default. |

WhatsApp Business Calling is an **override-only entitlement**. The
transport option is hidden when the tenant's entitlement certificate
explicitly denies `modalities.whatsapp`. A deployment with no
entitlements configured keeps the option visible; the control-plane
`addTrunk` / `updateTrunk` procedures apply the same deny-on-false gate,
so the UI and the API agree.

### WhatsApp webhook callback URL and verify token

Once a trunk is saved with `transport: whatsapp`, the screen shows two
values fetched from the control plane:

- **Callback URL**
- **Verify token**

Both are **derived server-side from the trunk id**. Nothing is stored
and nothing is generated for you to keep safe — they exist as soon as
the trunk row exists, and they are stable for that trunk. If the panel
reports the URL as unavailable, the trunk has not yet been saved with
the WhatsApp transport.

Paste both into the Meta app console under **Webhooks →
whatsapp_business_account**, then subscribe the app to the `calls` and
`messages` fields. Without the `calls` subscription, inbound WhatsApp
calls never reach the trunk.

## Carrier and SBC fields, ports and registration

These apply to `sip` trunks only.

| Field | Purpose |
| ----- | ------- |
| `trunk_id` | Stable identifier used by the dialplan, the routing blob and the derived webhook URL. Required. |
| `display_name` | Label shown in the console. |
| `sbc_ip` | Carrier SBC reachability target — where REGISTER and OPTIONS are sent. Required; validated as a host. |
| `domain` | SIP domain used in REGISTER. Often the same host as the SBC. |
| `proxy` | Outbound proxy as `host:port` or a `sip:` URI. |
| `source_pbx_port` | The local SIP listening port on our side. |
| `destination_sbc_port` | The carrier-assigned SBC port (carriers frequently assign a non-5060 port). |
| `sip_username` | Required. |
| `sip_password` | Secret — see [How secrets are stored and rotated](#how-secrets-are-stored-and-rotated). |
| `require_registration` | Whether this trunk registers to the carrier at all. IP-authenticated trunks leave this off. |
| `register_expires_sec` | Requested registration expiry. Validated as an integer between 30 and 86400. |

### Signalling and media addresses

The internal/external pairs exist because the gateway commonly sits
behind NAT: the internal address is what it binds, the external address
is what it advertises in SIP and SDP.

| Field | Purpose |
| ----- | ------- |
| `internal_sip_ip` / `external_sip_ip` | SIP bind address vs advertised address. |
| `internal_rtp_ip` / `external_rtp_ip` | RTP bind address vs advertised address. |
| `gateway_ip` | Gateway address for this trunk. |

All five are validated as IP addresses when present.

### Media

| Field | Purpose |
| ----- | ------- |
| `codec_preferences` | Ordered codec offer, e.g. `["PCMA","PCMU"]`. |
| `rtp_start_port` / `rtp_end_port` | RTP port range. Both must be valid ports and the start must be below the end. |
| `channel_limit` | Concurrent channel cap for the trunk. `0` is accepted and means **no cap** — reporting and the exception monitor only enforce a cap when `channel_limit > 0`. |
| `media_mode` | `proxy` — the gateway terminates RTP itself. `direct` — the gateway is signalling-only and the agent runtime terminates RTP for AI dialplans on this trunk. Direct mode advertises an agent-runtime address in the `200 OK`; use `proxy` for carriers or SBCs that mangle or reject NAT'd SDP. |
| `reject_audio_file` | Audio played when a call is rejected. |
| `auto_bargein_mode` | `energy`, `vad` or `none`. |
| `auto_bargein_aggressiveness` | `0`–`3`. |

### Realm and compliance headers

`realm` classifies what sits on the far end of the trunk. It drives
transfer policy, the implicit toolset advertised to in-call LLMs, and
CDR billing categorisation.

| Realm | Meaning |
| ----- | ------- |
| `internal` | The tenant's own PBX / extensions (FreeSWITCH or BYO). |
| `external` | PSTN or SIP carrier. Per-minute billing applies. |
| `ai_vendor` | An AI vendor (Vapi, LiveKit, Twilio, Daily, Chime) acting as the agent leg. |

`compliance_headers_mode` controls identity headers on outbound
INVITEs:

- `full` — include `Remote-Party-ID`, `P-Asserted-Identity` and
  `P-Access-Network-Info`. This is the regulator-friendly default.
- `minimal` — suppress all three. Use this for carriers that drop calls
  when the customer edge asserts identity.

## Pinning a regional SIP edge for data residency

`sip_edge_id` pins the trunk's signalling to a specific regional SIP
edge. The picker lists the enabled rows from the platform's `sip_edge`
registry, plus a **direct** option.

- Choosing a named edge routes this trunk's SIP through that region,
  which is how you keep signalling inside a residency boundary.
- Leaving the selector on the direct option stores `null` — no edge is
  pinned and the trunk talks to the carrier directly.

Selecting an edge also patches the trunk's address fields to that
edge's values, so the addresses you advertise to the carrier match the
edge you pinned. Whitelist the edge on the carrier side before you
save, otherwise the carrier will see traffic from an address it does
not recognise.

## Inbound routing precedence: DID rule, trunk rule, default park

The **effective route** panel is not a guess: the control plane's
`admin.dialplanRoutes` procedure reads the same rules blob that the
dialplan module routes on, and applies the same precedence. For each
trunk it returns a `default` decision plus a list of per-DID
`overrides`.

Each decision carries a `reason` telling you which rule won:

| `reason` | Meaning |
| -------- | ------- |
| `did_rule` | A rule specific to the dialled number matched. This wins over everything else. |
| `trunk_rule` | No DID-specific rule matched, so the trunk-level routing target applied. |
| `default_park` | Nothing matched. The action is `park` and **the caller hears silence**. |

The decision also reports the `action` and `args` the engine will
execute, and an `agentId` when the action is
`ai_bidirectional_stream`. The panel renders these as a sentence — the
target agent's name, the vector program, or an explicit warning for
`park`.

The trunk-level fields that feed a `trunk_rule` decision are:

| Field | Effect |
| ----- | ------ |
| `vdn_id` | Route inbound calls on this trunk to a VDN's vector program. **Mutually exclusive with the agent binding — when `vdn_id` is set, the VDN/vector wins.** |
| `agent_id` | The agent to route to. Picked from a list of every agent in the org, drafts and external vendor bridges included, because an external bridge is typically never "published" yet is exactly what a trunk points at. |
| `inbound_skill_id` | Default ACD skill for inbound calls that do not match a more specific dispatch rule. Empty means no ACD fallback on this trunk. Only active skills are offered, so routing can never target a soft-deleted queue. |
| `inbound_webhook_url` | HTTP(S) inbound webhook. |
| `inbound_ai_quic_url` | QUIC / MoQ inbound AI stream endpoint. |
| `inbound_ai_websocket_url` | WebSocket inbound AI stream endpoint. |
| `api_bearer_token` | Bearer token presented on inbound callouts. |

An empty `park` decision is the single most common cause of "the call
connects and then there is dead air". If the panel shows
`PARK — no rule matches`, the trunk has no routing target and no DID
rule covers the number being called.

### Why the panel lags a save

The control plane rebuilds the rules blob fire-and-forget after a
trunk save, so the screen waits briefly before re-reading the routes
after each save. The panel is advisory: if the query fails it is
silently skipped rather than blocking the screen.

## How secrets are stored and rotated

Secret fields are `sip_password`, `wa_access_token` and
`wa_app_secret`. Redis, sealed, is canonical for all three.

The rules are the same for every one of them:

1. **Stored secrets are never hydrated into the form.** When you expand
   a trunk, the secret inputs are blank even though a secret exists.
2. **Blank on save means "keep what is stored."** The control plane's
   `keepTrunkSecrets` handling reads an empty secret as no change.
3. **Typing a value rotates it.** The new value replaces the stored one.

Because of rule 2, the secret fields are only mandatory when the trunk
is first created. On edit they are optional, and the form's validation
reflects that: `sip_password` is required only on create, and the
WhatsApp `wa_access_token` likewise.

This also means you cannot read a secret back out of the console. If
you have lost a carrier password, rotate it with the carrier and type
the new one here.

## Deleting a trunk

Deleting a trunk from the list does not remove the row outright — the
console works against active trunks, and the list is filtered to
`active = true`. A deleted trunk therefore stops being offered as a
routing target and stops appearing in the list.

Before you delete, check the effective-route panel of any *other*
trunk that might have been pointed at the same agent or VDN, and
confirm the carrier has stopped sending traffic to the addresses this
trunk advertised. Inbound calls that still arrive for a trunk with no
matching rule end in `default_park`, which the caller experiences as
silence.

## Related

- [Authentication](/concepts/authentication) — API keys for the control-plane procedures behind this screen
- [Telemetry](/platform/telemetry) — the `trunk` label on metrics and CDRs comes from `trunk_id`
- [Telephony metrics](/glossary/metrics) — channel limits, trunk utilisation and ASR
