> ## 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.

# Worked SIP hook chains

> Worked chains for the 22 Jambonz call-hook routes: SIP-trunk auth (register → trunk auth → lookup), inbound routing (dial-confirm → callback-answer → feature-code → queue-wait), AI-agent bridge + quality ingest, and mailbox fallback (voicemail pin/store, pause-elapsed).

## Worked SIP hook chains

Jambonz calls these hook URLs over HTTP; every body below is the actual POST payload the Devotel softswitch sends to Orbit, not a payload you send to Orbit. The samples exist so a SIP-trunking operator can see which hook fires at which stage and what shape to expect on the wire when chaining to an external softswitch. Use raw cURL (or any HTTP client) to script these — the Node/Python/Go SDKs do not ship a typed helper for the Jambonz hook surface.

### Chain 1 — SIP trunk auth: Register auth → Customer trunk auth → Lookup

The softswitch challenges every new REGISTER, then every inbound INVITE, then routes the call.

#### 1a. REGISTER auth (softphone or BYO-PBX binding)

<Note>
  `POST /api/v1/jambonz/auth/register`
</Note>

Fired once per REGISTER the softswitch challenges. Orbit answers with the A1 digest hash and algorithm so the SBC can verify the UA locally — never a 5xx, always a clean `ok`/`fail`.

```bash cURL theme={null}
curl -X POST "https://api.orbit.devotel.io/api/v1/jambonz/auth/register" \
  -H "jambonz-signature: t=1756480000,v1=REDACTED" \
  -H "Content-Type: application/json" \
  -d '{
  "username": "trunk_acme-pbx",
  "realm": "orbit.devotel.io",
  "method": "REGISTER",
  "uri": "sip:orbit.devotel.io",
  "nonce": "9b0c7f…",
  "expires": 600,
  "source_ip": "203.0.113.44",
  "user_agent": "Asterisk PBX 20.5"
}'
```

**Response:** `200 { "status": "ok", "a1_hash": "1ad1c4…", "algorithm": "MD5", "expires": 60 }` — the `expires` clause only appears when the PBX asked for an interval below the healthy floor; otherwise it is omitted and the PBX's own value stands. Wrong digest → `401 { "status": "fail" }`.

#### 1b. Customer trunk auth (INVITE/REGISTER digest on the shared carrier)

<Note>
  `POST /api/v1/jambonz/customer-trunk-auth`
</Note>

The account-scoped auth hook for the `customer-trunks` carrier — it sees BOTH the INVITE (incoming call) and REGISTER (binding) digest claims of every BYO-PBX trunk.

```bash cURL theme={null}
curl -X POST "https://api.orbit.devotel.io/api/v1/jambonz/customer-trunk-auth" \
  -H "jambonz-signature: t=1756480005,v1=REDACTED" \
  -H "Content-Type: application/json" \
  -d '{
  "method": "INVITE",
  "username": "trunk_acme-pbx",
  "realm": "inbound.devotel.io",
  "uri": "sip:+14155550100@main.devotel.io",
  "from": "sip:+49891234567@203.0.113.44",
  "source_ip": "203.0.113.44"
}'
```

**Response:** returns `{ "status": "ok", "password": "<trunk-digest-secret>" }` so the SBC can verify the PBX's digest locally; when the PBX asked for an interval below the healthy floor the same `expires` floor clause rides along. Unknown digest username → `401 { "status": "fail" }` and the source IP is probe-blacklisted at the edge. Trunk with no digest password configured → `403 { "status": "fail", "message": "no_password_configured" }`. Non-INVITE/non-REGISTER methods (OPTIONS keepalives) short-circuit to a bare `{ "status": "ok" }`.

#### 1c. Lookup — resolve the dialed party to a route

<Note>
  `POST /api/v1/jambonz/lookup`
</Note>

Fired on every inbound INVITE once auth clears. `to` is the dialed party (E.164, SIP URI, or carrier-prefixed digit run); `originating_sip_trunk_name` discriminates inbound-PSTN vs browser-softphone outbound; `sip.headers` carries any custom SIP headers the originating UA sent (e.g. `X-Tenant-ID`, `X-Devotel-Call-Id` set at softphone-initiation).

```bash cURL theme={null}
curl -X POST "https://api.orbit.devotel.io/api/v1/jambonz/lookup" \
  -H "jambonz-signature: t=1756480006,v1=REDACTED" \
  -H "Content-Type: application/json" \
  -d '{
  "to": "+14155550100",
  "from": "+49891234567",
  "call_sid": "call_01HWKJF…",
  "originating_sip_trunk_name": "devotel-sbc",
  "source_ip": "203.0.113.44",
  "sip": {
    "headers": {
      "X-Tenant-ID": "org_acme",
      "X-Devotel-Call-Id": "call_01HWKJF…"
    }
  }
}'
```

**Response:** a Jambonz verb array for the resolved route — e.g. a `dial` verb fanned out across the matched inbound-route's SIP usernames, or the business-hours fallback. `Lookup-next` (`POST /api/v1/jambonz/lookup-next`) is the sequential-ring continuation: it advances to the next username in the route's fan-out after a no-answer timeout, carrying the same body shape plus the attempt state.

### Chain 2 — inbound routing: Dial-confirm → Callback-answer → Feature-code → Queue-wait

Once the route resolves, the softswitch drives the call through these verb hooks.

#### 2a. Dial-confirm — screen the outbound arm before connecting

<Note>
  `POST /api/v1/jambonz/dial-confirm`
</Note>

Fires when a `dial` arm is about to answer so Orbit can last-moment veto or confirm destination eligibility (TCPA/time-window guard) before the caller hears the arm ring.

```bash cURL theme={null}
curl -X POST "https://api.orbit.devotel.io/api/v1/jambonz/dial-confirm" \
  -H "jambonz-signature: t=1756480011,v1=REDACTED" \
  -H "Content-Type: application/json" \
  -d '{
  "call_sid": "call_01HWKJF…",
  "from": "+49891234567",
  "to": "+14155550102"
}'
```

**Response:** a verb array — typically a one-word confirm or the polite-decline verbs. On any transient failure Orbit fails CLOSED with a 503 so the leg is never connected without confirmation.

#### 2b. Callback-answer — outbound callback leg answered

<Note>
  `POST /api/v1/jambonz/callback-answer`
</Note>

Fires when a queued callback (requested by the caller during queue-wait) reaches the customer's phone and goes live — Orbit emits the verbs that bridge the customer to the reserved agent.

```bash cURL theme={null}
curl -X POST "https://api.orbit.devotel.io/api/v1/jambonz/callback-answer" \
  -H "jambonz-signature: t=1756480022,v1=REDACTED" \
  -H "Content-Type: application/json" \
  -d '{
  "call_sid": "call_01HWKMY…",
  "from": "+14155550100",
  "to": "+49891234567"
}'
```

**Response:** the bridge verb array; the sibling `Pause-elapsed` hook (chain 4) is what fires instead when the caller held in-queue and the pause timer expired without an agent.

#### 2c. Feature-code — star-code interception mid-call

<Note>
  `POST /api/v1/jambonz/feature-code`
</Note>

Fires when the caller dials a configured star code (e.g. `*97` mailbox, `*72` DND toggle) — the softswitch sends the code plus the in-call `username` binding it to a SIP credential.

```bash cURL theme={null}
curl -X POST "https://api.orbit.devotel.io/api/v1/jambonz/feature-code" \
  -H "jambonz-signature: t=1756480030,v1=REDACTED" \
  -H "Content-Type: application/json" \
  -d '{
  "call_sid": "call_01HWKJF…",
  "code": "*97",
  "username": "acme-front-desk"
}'
```

**Response:** the verb array implementing the code (park the call, open the mailbox TUI, toggle DND), or a "feature not enabled" announcement when the credential has no such code.

#### 2d. Queue-wait — caller holding in queue

<Note>
  `POST /api/v1/jambonz/queue-wait`
</Note>

Fires per queue-duration tick while the caller holds; Orbit replies with the announcement/mus verbs plus position-in-queue, and may offer the callback escape. The siblings `Queue-action` and `Queue-overflow` fire on the caller's menu choice and on wait-timeout overflow respectively; `Conference-wait-music` feeds the hold stream.

```bash cURL theme={null}
curl -X POST "https://api.orbit.devotel.io/api/v1/jambonz/queue-wait" \
  -H "jambonz-signature: t=1756480045,v1=REDACTED" \
  -H "Content-Type: application/json" \
  -d '{
  "call_sid": "call_01HWKJF…",
  "queue_name": "support-tier-1",
  "position": 3,
  "wait_seconds": 47
}'
```

**Response:** the next wait-cycle verb array (`say` position in queue, `play` hold music, `gather` for the callback opt-in).

### Chain 3 — AI voice agent: Bridge-to-lk / Supervisor-bridge → Quality-ingest

The AI-agent pillars bridge the live PSTN call into a LiveKit room; supervisors join the same room in listen/whisper/barge/conference modes; the media layer pushes call quality telemetry into the ingest.

#### 3a. Bridge-to-lk — drop the PSTN call into the LiveKit room

<Note>
  `POST /api/v1/jambonz/bridge-to-lk`
</Note>

```bash cURL theme={null}
curl -X POST "https://api.orbit.devotel.io/api/v1/jambonz/bridge-to-lk" \
  -H "jambonz-signature: t=1756480050,v1=REDACTED" \
  -H "Content-Type: application/json" \
  -d '{
  "call_sid": "call_01HWKJF…",
  "room": "lk_call_01HWKJF",
  "from": "+49891234567"
}'
```

**Response:** the `dial` verb toward the LiveKit SIP URI (`X-LK-Room` header) — the AI agent joins the room as a conference participant and converses with the caller in real time.

#### 3b. Supervisor-bridge — join a supervisor to the live call

<Note>
  `POST /api/v1/jambonz/supervisor-bridge?callId=…&mode=listen|whisper|barge|conference`
</Note>

Verb hook landing the supervisor's leg into the `conf_<callId>` room. The mode drives the audio path: `listen` (muted supervisor), `whisper` (audible to the agent only), `barge` and `conference` (audible to all — `conference` is the consult-and-conference 3-way mode).

```bash cURL theme={null}
curl -X POST "https://api.orbit.devotel.io/api/v1/jambonz/supervisor-bridge?callId=call_01HWKJF%E2%80%A6&mode=whisper&agentCallSid=call_01HWKAG%E2%80%A6" \
  -H "jambonz-signature: t=1756480055,v1=REDACTED" \
  -H "Content-Type: application/json" \
  -d '{}'
```

**Response:** a single `conference` verb — `{ "verb": "conference", "name": "conf_call_01HWKJF…", "muted": false, "coachCallSid": "call_01HWKAG…" }` for whisper. Invalid query → `hangup`.

#### 3c. Quality-ingest — RTCP-XR telemetry for the call legs

<Note>
  `POST /api/v1/jambonz/quality-ingest`
</Note>

Separate from the call-control hooks: the orbit-media layer pushes per-leg quality samples (HMAC-signed) into this ingest; the voice-quality surfaces render from it.

```bash cURL theme={null}
curl -X POST "https://api.orbit.devotel.io/api/v1/jambonz/quality-ingest" \
  -H "X-Orbit-Signature: t=1756480062,v1=REDACTED" \
  -H "Content-Type: application/json" \
  -d '{
  "event": "rtcp_xr",
  "call_id": "call_01HWKJF…",
  "tenant_id": "org_acme",
  "recorded_at": "2026-08-29T12:14:02Z",
  "metrics": {
    "mos_score": 4.2,
    "jitter_avg_ms": 11,
    "packet_loss_pct": 0.4,
    "codec": "opus",
    "carrier": "devotel-sbc"
  }
}'
```

**Response:** `200` with the stored sample ack; a missing platform HMAC key fail-closes every POST with a 503 (the quality surface then reports collection as not enabled, not as empty).

### Chain 4 — Mailbox fallback: Voicemail pin/store, Pause-elapsed fallback

When the route ends in a mailbox rather than an agent, the TUI challenges for the PIN and stores the message; the pause/callback fallback covers callers who held past the timer.

#### 4a. Voicemail-pin — mailbox TUI PIN challenge

<Note>
  `POST /api/v1/jambonz/voicemail-pin`
</Note>

```bash cURL theme={null}
curl -X POST "https://api.orbit.devotel.io/api/v1/jambonz/voicemail-pin?username=acme-front-desk&attempt=1" \
  -H "jambonz-signature: t=1756480070,v1=REDACTED" \
  -H "Content-Type: application/json" \
  -d '{
  "call_sid": "call_01HWKJF…",
  "username": "acme-front-desk",
  "digits": "2468"
}'
```

**Response:** the TUI verb array — the next prompt (`say` "enter your PIN again" / "you have N messages") or the polite-decline verbs after the attempt ceiling. The PIN itself is a tenant-owned mailbox control stored as a salted hash; it never leaves the tenant boundary.

#### 4b. Voicemail-store — persist the recorded message

<Note>
  `POST /api/v1/jambonz/voicemail-store`
</Note>

Fires when the mailbox recording completes; Orbit persists the message rows so the mailbox surfaces (envelope/unread counters, playback) see the recording immediately.

```bash cURL theme={null}
curl -X POST "https://api.orbit.devotel.io/api/v1/jambonz/voicemail-store" \
  -H "jambonz-signature: t=1756480075,v1=REDACTED" \
  -H "Content-Type: application/json" \
  -d '{
  "call_sid": "call_01HWKJF…",
  "to": "+14155550100",
  "from": "+49891234567",
  "recording_url": "https://recordings.orbit.devotel.io/vm/01HWKVM8.mp3",
  "duration_seconds": 26
}'
```

**Response:** `200` ack; the message appears in the mailbox for the matched username/DID.

#### 4c. Pause-elapsed — held caller's pause timer fired

<Note>
  `POST /api/v1/jambonz/pause-elapsed`
</Note>

Fires when a caller who chose "hold for an agent" exhausts the pause window without an agent reserving them — the fallback that decides between callback-offer, mailbox, or a polite hangup (paired with `Callback-answer` in chain 2 and `Mid-queue-voicemail` for an in-queue mailbox offer).

```bash cURL theme={null}
curl -X POST "https://api.orbit.devotel.io/api/v1/jambonz/pause-elapsed" \
  -H "jambonz-signature: t=1756480081,v1=REDACTED" \
  -H "Content-Type: application/json" \
  -d '{
  "call_sid": "call_01HWKJF…",
  "queue_name": "support-tier-1",
  "elapsed_seconds": 300
}'
```

**Response:** the fallback verb array (callback offer → mailbox → polite good-bye).

***

**Webhook authentication.** Every hook above is HMAC-signed: the softswitch sends `jambonz-signature: t=<unix>,v1=<hex>` (quality-ingest uses `X-Orbit-Signature` with the same scheme) over the raw body. Rejects return a clean 4xx envelope (`{ "status": "fail", "message": "…" }`), never a 5xx — that keeps the SIP transaction clean at the edge. See [the Jambonz softswitch concept page](/concepts/jambonz-softswitch) for the account/registration model.

**Idempotency.** Every hook accepts the standard `Idempotency-Key` header and replays identically on retry — use the response's envelope exactly as computed (a 409 means the same key arrived twice mid-flight).
