# Hosted carrier answer URLs

> Mint the hosted answer URL a programmable-voice carrier fetches to route an inbound DID onto a SIP trunk, plus SIP edge selection and carrier source CIDRs.

Some carriers do not hand you an inbound DID as plain SIP. Instead of
sending an `INVITE` to your gateway, they fetch an HTTP URL when a call
arrives and execute the document that comes back. That document is what
tells the carrier to connect the call onto your TeleQuick trunk.

The trunk editor mints that URL for you. Open a saved trunk, find the
**Carrier answer URL** row under **SIP endpoint**, pick the dialect that
matches your carrier, and copy the value.

## When a carrier needs an answer URL

Use the answer URL when the carrier is a programmable-voice platform
rather than a wholesale SIP provider:

| Carrier | How it is used |
| ------- | -------------- |
| Piopiy | Required. Piopiy routes inbound DIDs through an answer URL. |
| vobiz | Application mode — set the URL on the vobiz application bound to the DID. |
| Plivo | Application mode — the Plivo application's answer URL. |
| Twilio | The "a call comes in" webhook on the number or on a TwiML app. |

A conventional SIP trunk that sends `INVITE`s straight at your
**Gateway IP** / **SBC IP** needs none of this. For those trunks, ignore
the row.

## Why the row only appears on a saved trunk

The **Carrier answer URL** row renders only when the trunk already
exists (`initial.trunk_id` is set). That is not cosmetic. Minting the
URL is a `admin.carrierAnswerUrl` query that reads the trunk's stored
config out of Redis and fails with `NOT_FOUND` if there is no row for
that trunk id. There is nothing to resolve against until you have saved
the trunk once.

So the order is: fill in the SIP endpoint fields → **Save** → reopen the
trunk → copy the answer URL.

## The four dialects and what each returns

The dialect selector to the left of the URL field changes the path, and
therefore the shape of the document the carrier receives. Each carrier
parses only its own format, so the dialect must match the console you
are pasting into:

- `piopiy`
- `vobiz`
- `plivo`
- `twilio`

Whichever dialect you choose, the document's job is the same: connect
the inbound call to the SIP user registered for this trunk. The dialect
only decides the wire format the carrier expects to read.

If you paste a `twilio` URL into a Plivo application (or vice versa),
the carrier fetches the URL successfully and then fails to parse the
response. Re-copy with the correct dialect selected.

## Pasting it into the carrier console

1. Select the dialect for your carrier.
2. Click **Copy**. (If the clipboard is unavailable in your browser, the
   field is read-only but selectable — focus it and it selects the whole
   value.)
3. In the carrier console, set it as the answer URL / application URL /
   inbound webhook for the DID you want to land on this trunk.

The console hint says it plainly: *paste into the carrier console (Piopiy
answer URL, vobiz/Plivo application, Twilio webhook) to route inbound
calls to this trunk*.

Because the URL is derived from the trunk rather than stored, you can
re-open the trunk and copy it again at any time. There is no "reveal
secret" step and no one-time display.

## How the URL resolves to a trunk and its SIP user

`admin.carrierAnswerUrl` takes `{ orgId, trunkId, dialect }` and:

1. Reads the trunk's config from the `trunks_data` hash in Redis.
2. Rejects the request with `NOT_FOUND` when there is no such trunk, and
   with `INTERNAL_SERVER_ERROR` when the stored config cannot be parsed.
3. Compares the config's `tenant_id` against `orgId`. A mismatch is a
   `FORBIDDEN` — you cannot mint an answer URL for another tenant's
   trunk.
4. Returns `{ path, url }`, where the URL is the hosted answer endpoint
   plus the derived path for that dialect and trunk.

The key inside the path is **derived, not stored**. Nothing is persisted
at mint time, and minting the URL twice produces the same value. The
same pattern is used for the WhatsApp webhook URL, where the derived key
also doubles as the verify token.

When the carrier fetches the URL, the returned document connects the
call to the SIP user registered for that trunk — i.e. the **SIP
username** you configured on the trunk, with the password held only in
sealed Redis.

### Inbound routing after the call lands

The answer URL gets the call onto the trunk. Where it goes from there is
the trunk's own inbound routing, rebuilt into the dialplan every time you
save (`rebuildDialplanRulesSafe`): a `vdn_id` produces a `vector:` rule,
otherwise `agent_id` produces an `ai_bidirectional_stream:` rule. If you
leave both empty, the trunk has no inbound route and the call parks on
silence.

## Choosing a SIP edge for residency

**SIP edge** sits in the same **SIP endpoint** block. It is a select
populated from the platform `sip_edge` registry — enabled rows only,
readable by every tenant, ordered by name. Leaving it on the direct
option means the trunk talks to the carrier without an edge in the path.

Picking an edge is a data-residency decision: signalling for this trunk
traverses the selected regional edge. Selecting an edge patches the
trunk's related address fields (`edgeFieldPatch`), and the hint line
beneath the grid describes the edge you chose. `sip_edge_id` is carried
into the sealed trunk payload, and `withSipEdge` merges it on save, so
an edge already attached to a stored trunk survives a re-save that does
not touch the field.

## Bypassing the 407 gate with carrier source CIDRs

By default a trunk authenticates the carrier — the engine challenges
inbound requests and expects credentials (the SIP `407` gate). Some
carriers, including the programmable-voice platforms that use answer
URLs, will not answer a challenge. For those, authorise the carrier by
source address instead.

**Allowed source CIDRs** (`acl_allow_cidrs`) is a comma-separated list
of IPs or CIDR blocks, e.g. `203.0.113.0/24`. Traffic arriving from a
listed source is accepted on this trunk without the credential
challenge. The form validates every entry and reports the first bad one
by name; an empty list is written as `null`, meaning "no bypass — the
gate applies".

On save, `withCarrierAllowCidrs` merges the list into the sealed trunk
payload alongside the SIP edge, so the carrier's own published ranges can
be maintained without being clobbered by an unrelated edit.

Scope the list to the exact ranges your carrier publishes. Every address
you list can originate calls on this trunk without presenting
credentials.

## Saving, secrets, and hot reload

The trunk form is a dual write and the answer URL depends on both halves
landing:

- The non-secret Postgres mirror row is written **server-side first** by
  `admin.addTrunk` / `admin.updateTrunk` (org-scoped and audited). If the
  mirror write fails, nothing goes live in Redis alone.
- The full config — including secrets — is then sealed under the org KEK
  and written to the `trunks_data` hash, which is what
  `admin.carrierAnswerUrl` later reads.
- `sip_password` and `api_bearer_token` are never hydrated back into the
  form. Blank means "keep what is stored" (`keepTrunkSecrets`); typing a
  value rotates it. The browser sends them as `null` on the mirror row,
  which also clears any pre-existing plaintext there.
- Saving pushes the trunk id onto the SIP reload queue so `mod_sip`
  picks up the change without an engine bounce, and rebuilds the dialplan
  rules.

`admin.addTrunk` upserts by `(org_id, trunk_id)`, so re-saving an
existing trunk id — for example after a bulk import — updates the
existing row rather than creating a duplicate, and the answer URL for
that trunk id stays the same.

## Related

- [Authentication](/concepts/authentication)
- [Telemetry](/platform/telemetry)
