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

# Voice API

> Manage calls, SIP trunks, IVR flows, conferences, and voicemail

# Voice API

Build voice applications with Orbit's Voice API. Make and receive calls, connect AI agents, configure SIP trunks, build IVR menus, host conference calls, and manage voicemail — all through a unified API.

**Base path:** `/api/v1/voice`

***

## Calls

### Initiate a Call

`POST /api/v1/voice/calls`

Start an outbound call. Route it to an AI voice agent, an IVR flow, or a raw SIP destination.

<ParamField body="to" type="string" required>
  Destination phone number in E.164 format
</ParamField>

<ParamField body="from" type="string">
  Caller ID — must be a number registered in your Orbit account. If omitted, the server falls back to your tenant's default outbound number.
</ParamField>

<ParamField body="direction" type="string" default="outbound">
  Call direction: `outbound` or `inbound`.
</ParamField>

<ParamField body="agent_id" type="string">
  AI voice agent to handle the call. The agent must exist in your tenant and not be archived (omit for raw SIP or an Application-bound IVR).
</ParamField>

<ParamField body="record" type="boolean" default="false">
  Whether to record the call
</ParamField>

<ParamField body="amd" type="boolean" default="false">
  Request carrier-side Answering Machine Detection for the outbound call.
</ParamField>

<ParamField body="campaignId" type="string">
  Campaign ID for spend-cap tracking. Recorded on the call log for attribution.
</ParamField>

<ParamField body="orgTimezone" type="string">
  IANA timezone (e.g. `America/New_York`) used for the TCPA quiet-hours check on outbound calls.
</ParamField>

<ParamField body="metadata" type="object">
  Arbitrary key-value pairs attached to the call
</ParamField>

<ParamField body="answer_url" type="string">
  HTTPS URL for programmable call-control. When the call is answered, Orbit fetches this URL and expects a JSON verb sequence — either `{ "verbs": [...] }` or a bare array — describing the flow (`say`, `gather`, `dial`, and so on). Must use the `https://` scheme. Mutually exclusive with `staticVerbs`.
</ParamField>

<ParamField body="answer_method" type="string" default="POST">
  HTTP method used to fetch `answer_url`: `POST` or `GET`.
</ParamField>

<ParamField body="answer_fallback" type="string" default="safe-default">
  What the call falls back to when the `answer_url` fetch fails (timeout, non-2xx, or malformed body): `safe-default`, `voicemail`, or `decline`.
</ParamField>

<ParamField body="staticVerbs" type="array">
  Inline call-control verb sequence served without a remote fetch. Each entry is an object with a `verb` field plus its verb-specific params (e.g. `{ "verb": "say", "text": "Hello" }`). Up to 50 verbs. Mutually exclusive with `answer_url`.
</ParamField>

<Note>
  For per-call programmable control, use `answer_url` or `staticVerbs` above — supply at most one of them. There are no `webhook_url`, `ivr_id`, or `max_duration` fields; bind IVR menus and TTS prompts to the Application or IVR Flow on the number instead. Any field not listed above is ignored by the server.
</Note>

<RequestExample>
  <CodeGroup>
    ```bash cURL theme={null}
    curl -X POST "https://api.orbit.devotel.io/api/v1/voice/calls" \
      -H "X-API-Key: dv_live_sk_your_key_here" \
      -H "Content-Type: application/json" \
      -d '{
      "to": "+14155552671",
      "from": "+12025551234",
      "agent_id": "agt_abc123",
      "record": true,
      "amd": true
    }'
    ```

    ```typescript Node.js theme={null}
    import { Orbit } from '@devotel/orbit-sdk'

    const orbit = new Orbit({ apiKey: process.env.ORBIT_API_KEY! })

    const call = await orbit.voice.calls.create({
      to: '+14155552671',
      from: '+12025551234',
      agent_id: 'agt_abc123',
      record: true,
      amd: true,
    })
    console.log(call.data.id)
    ```

    ```python Python theme={null}
    import os, requests

    headers = {"X-API-Key": os.environ["ORBIT_API_KEY"], "Content-Type": "application/json"}
    r = requests.post("https://api.orbit.devotel.io/api/v1/voice/calls", headers=headers, json={
      "to": "+14155552671",
      "from": "+12025551234",
      "agent_id": "agt_abc123",
      "record": True,
      "amd": True
    })
    print(r.json())
    ```

    ```go Go theme={null}
    package main

    import (
    	"bytes"
    	"net/http"
    	"os"
    )

    func main() {
    	req, _ := http.NewRequest("POST", "https://api.orbit.devotel.io/api/v1/voice/calls", bytes.NewBuffer([]byte(`{
      "to": "+14155552671",
      "from": "+12025551234",
      "agent_id": "agt_abc123",
      "record": true,
      "amd": true
    }`)))
    	req.Header.Set("X-API-Key", os.Getenv("ORBIT_API_KEY"))
    	req.Header.Set("Content-Type", "application/json")
    	http.DefaultClient.Do(req)
    }
    ```

    ```php PHP theme={null}
    <?php
    $ch = curl_init('https://api.orbit.devotel.io/api/v1/voice/calls');
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST');
    curl_setopt($ch, CURLOPT_HTTPHEADER, [
      'X-API-Key: ' . getenv('ORBIT_API_KEY'),
      'Content-Type: application/json',
    ]);
    curl_setopt($ch, CURLOPT_POSTFIELDS, <<<JSON
    {
      "to": "+14155552671",
      "from": "+12025551234",
      "agent_id": "agt_abc123",
      "record": true,
      "amd": true
    }
    JSON);
    echo curl_exec($ch);
    ```
  </CodeGroup>
</RequestExample>

<ResponseExample>
  ```json 201 theme={null}
  {
    "data": {
      "id": "call_abc123",
      "to": "+14155552671",
      "from": "+12025551234",
      "agent_id": "agt_abc123",
      "status": "initiating",
      "record": true,
      "created_at": "2026-03-08T12:00:00Z"
    },
    "meta": {
      "request_id": "req_call_001",
      "timestamp": "2026-03-08T12:00:00Z"
    }
  }
  ```
</ResponseExample>

### List Calls

`GET /api/v1/voice/calls`

Retrieve call logs with cursor-based pagination and optional filters.

<ParamField query="cursor" type="string">
  Cursor for pagination
</ParamField>

<ParamField query="limit" type="integer" default="25">
  Results per page (max 200)
</ParamField>

<ParamField query="status" type="string">
  Filter by call status. Free-text (up to 40 characters) so carrier-reported
  values pass through unchanged; an unrecognized value simply matches its own
  rows (or none) rather than returning an error. Repeat the parameter to filter
  by several statuses at once (e.g. `?status=completed&status=busy`).

  Canonical lifecycle values in use: `queued`, `initiated`, `ringing`,
  `in-progress`, `active`, `answered`, `completed`, `busy`, `no-answer`,
  `failed`, `canceled`, `cancelled`, `missed`, `transferred`, `hangup_failed`.
</ParamField>

<ParamField query="from" type="string">
  Filter by caller number
</ParamField>

<ParamField query="to" type="string">
  Filter by destination number
</ParamField>

<RequestExample>
  <CodeGroup>
    ```bash cURL theme={null}
    curl -X GET "https://api.orbit.devotel.io/api/v1/voice/calls?status=completed&limit=20" \
      -H "X-API-Key: dv_live_sk_your_key_here"
    ```

    ```typescript Node.js theme={null}
    import { Orbit } from '@devotel/orbit-sdk'

    const orbit = new Orbit({ apiKey: process.env.ORBIT_API_KEY! })

    const calls = await orbit.voice.calls.list({ status: 'completed', limit: 20 })
    console.log(calls.data)
    ```

    ```python Python theme={null}
    import os, requests

    headers = {"X-API-Key": os.environ["ORBIT_API_KEY"]}
    r = requests.get("https://api.orbit.devotel.io/api/v1/voice/calls", headers=headers,
                     params={"status": "completed", "limit": 20})
    print(r.json())
    ```

    ```go Go theme={null}
    package main

    import (
    	"net/http"
    	"os"
    )

    func main() {
    	req, _ := http.NewRequest("GET", "https://api.orbit.devotel.io/api/v1/voice/calls?status=completed&limit=20", nil)
    	req.Header.Set("X-API-Key", os.Getenv("ORBIT_API_KEY"))
    	http.DefaultClient.Do(req)
    }
    ```

    ```php PHP theme={null}
    <?php
    $ch = curl_init('https://api.orbit.devotel.io/api/v1/voice/calls?status=completed&limit=20');
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($ch, CURLOPT_HTTPHEADER, [
      'X-API-Key: ' . getenv('ORBIT_API_KEY'),
    ]);
    echo curl_exec($ch);
    ```
  </CodeGroup>
</RequestExample>

### Get Call Details

`GET /api/v1/voice/calls/{id}`

Retrieve full details of a call including status, duration, recording URL, and agent interaction summary.

<RequestExample>
  <CodeGroup>
    ```bash cURL theme={null}
    curl -X GET "https://api.orbit.devotel.io/api/v1/voice/calls/call_abc123" \
      -H "X-API-Key: dv_live_sk_your_key_here"
    ```

    ```typescript Node.js theme={null}
    const call = await orbit.voice.calls.get('call_abc123')
    console.log(call.data)
    ```

    ```python Python theme={null}
    import os, requests
    headers = {"X-API-Key": os.environ["ORBIT_API_KEY"]}
    r = requests.get("https://api.orbit.devotel.io/api/v1/voice/calls/call_abc123", headers=headers)
    print(r.json())
    ```

    ```go Go theme={null}
    package main

    import (
    	"net/http"
    	"os"
    )

    func main() {
    	req, _ := http.NewRequest("GET", "https://api.orbit.devotel.io/api/v1/voice/calls/call_abc123", nil)
    	req.Header.Set("X-API-Key", os.Getenv("ORBIT_API_KEY"))
    	http.DefaultClient.Do(req)
    }
    ```

    ```php PHP theme={null}
    <?php
    $ch = curl_init('https://api.orbit.devotel.io/api/v1/voice/calls/call_abc123');
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($ch, CURLOPT_HTTPHEADER, ['X-API-Key: ' . getenv('ORBIT_API_KEY')]);
    echo curl_exec($ch);
    ```
  </CodeGroup>
</RequestExample>

### Hang Up a Call

`POST /api/v1/voice/calls/{id}/hangup`

Terminate an active call. AI agents receive a graceful shutdown signal before disconnection.

<RequestExample>
  <CodeGroup>
    ```bash cURL theme={null}
    curl -X POST "https://api.orbit.devotel.io/api/v1/voice/calls/call_abc123/hangup" \
      -H "X-API-Key: dv_live_sk_your_key_here"
    ```

    ```typescript Node.js theme={null}
    await orbit.voice.calls.hangup('call_abc123')
    ```

    ```python Python theme={null}
    import os, requests
    headers = {"X-API-Key": os.environ["ORBIT_API_KEY"]}
    r = requests.post("https://api.orbit.devotel.io/api/v1/voice/calls/call_abc123/hangup", headers=headers)
    print(r.json())
    ```

    ```go Go theme={null}
    package main

    import (
    	"net/http"
    	"os"
    )

    func main() {
    	req, _ := http.NewRequest("POST", "https://api.orbit.devotel.io/api/v1/voice/calls/call_abc123/hangup", nil)
    	req.Header.Set("X-API-Key", os.Getenv("ORBIT_API_KEY"))
    	http.DefaultClient.Do(req)
    }
    ```

    ```php PHP theme={null}
    <?php
    $ch = curl_init('https://api.orbit.devotel.io/api/v1/voice/calls/call_abc123/hangup');
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST');
    curl_setopt($ch, CURLOPT_HTTPHEADER, ['X-API-Key: ' . getenv('ORBIT_API_KEY')]);
    echo curl_exec($ch);
    ```
  </CodeGroup>
</RequestExample>

### Accept / Decline an Inbound Call

`POST /api/v1/voice/calls/{id}/accept`

Accept an incoming call before it auto-rings the configured number-handler. Use when the softphone or programmatic handler wants to answer in lieu of the default routing.

`POST /api/v1/voice/calls/{id}/decline`

Reject an incoming call. The carrier receives `486 Busy Here`.

<RequestExample>
  <CodeGroup>
    ```bash cURL theme={null}
    curl -X POST "https://api.orbit.devotel.io/api/v1/voice/calls/call_abc123/accept" \
      -H "X-API-Key: dv_live_sk_your_key_here"
    ```

    ```typescript Node.js theme={null}
    await orbit.voice.calls.accept('call_abc123')
    // or: await orbit.voice.calls.decline('call_abc123')
    ```

    ```python Python theme={null}
    import os, requests
    headers = {"X-API-Key": os.environ["ORBIT_API_KEY"]}
    r = requests.post("https://api.orbit.devotel.io/api/v1/voice/calls/call_abc123/accept", headers=headers)
    print(r.json())
    ```

    ```go Go theme={null}
    package main

    import (
    	"net/http"
    	"os"
    )

    func main() {
    	req, _ := http.NewRequest("POST", "https://api.orbit.devotel.io/api/v1/voice/calls/call_abc123/accept", nil)
    	req.Header.Set("X-API-Key", os.Getenv("ORBIT_API_KEY"))
    	http.DefaultClient.Do(req)
    }
    ```

    ```php PHP theme={null}
    <?php
    $ch = curl_init('https://api.orbit.devotel.io/api/v1/voice/calls/call_abc123/accept');
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST');
    curl_setopt($ch, CURLOPT_HTTPHEADER, ['X-API-Key: ' . getenv('ORBIT_API_KEY')]);
    echo curl_exec($ch);
    ```
  </CodeGroup>
</RequestExample>

### Mute / Unmute a Call Leg

`POST /api/v1/voice/calls/{id}/mute`

`POST /api/v1/voice/calls/{id}/unmute`

Mute or unmute the operator side of a connected call. Mute is per-leg — the participant on the other side keeps speaking and their audio still records.

<RequestExample>
  <CodeGroup>
    ```bash cURL theme={null}
    curl -X POST "https://api.orbit.devotel.io/api/v1/voice/calls/call_abc123/mute" \
      -H "X-API-Key: dv_live_sk_your_key_here"
    ```

    ```typescript Node.js theme={null}
    await orbit.voice.calls.mute('call_abc123')
    await orbit.voice.calls.unmute('call_abc123')
    ```

    ```python Python theme={null}
    import os, requests
    headers = {"X-API-Key": os.environ["ORBIT_API_KEY"]}
    r = requests.post("https://api.orbit.devotel.io/api/v1/voice/calls/call_abc123/mute", headers=headers)
    print(r.json())
    ```

    ```go Go theme={null}
    package main

    import (
    	"net/http"
    	"os"
    )

    func main() {
    	req, _ := http.NewRequest("POST", "https://api.orbit.devotel.io/api/v1/voice/calls/call_abc123/mute", nil)
    	req.Header.Set("X-API-Key", os.Getenv("ORBIT_API_KEY"))
    	http.DefaultClient.Do(req)
    }
    ```

    ```php PHP theme={null}
    <?php
    $ch = curl_init('https://api.orbit.devotel.io/api/v1/voice/calls/call_abc123/mute');
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST');
    curl_setopt($ch, CURLOPT_HTTPHEADER, ['X-API-Key: ' . getenv('ORBIT_API_KEY')]);
    echo curl_exec($ch);
    ```
  </CodeGroup>
</RequestExample>

### Hold / Unhold

`POST /api/v1/voice/calls/{id}/hold`

`POST /api/v1/voice/calls/{id}/unhold`

Place the remote party on hold (carrier-side music or silence depending on the trunk's `hold_music` configuration). Unhold restores audio in both directions.

<RequestExample>
  <CodeGroup>
    ```bash cURL theme={null}
    curl -X POST "https://api.orbit.devotel.io/api/v1/voice/calls/call_abc123/hold" \
      -H "X-API-Key: dv_live_sk_your_key_here"
    ```

    ```typescript Node.js theme={null}
    await orbit.voice.calls.hold('call_abc123')
    await orbit.voice.calls.unhold('call_abc123')
    ```

    ```python Python theme={null}
    import os, requests
    headers = {"X-API-Key": os.environ["ORBIT_API_KEY"]}
    r = requests.post("https://api.orbit.devotel.io/api/v1/voice/calls/call_abc123/hold", headers=headers)
    print(r.json())
    ```

    ```go Go theme={null}
    package main

    import (
    	"net/http"
    	"os"
    )

    func main() {
    	req, _ := http.NewRequest("POST", "https://api.orbit.devotel.io/api/v1/voice/calls/call_abc123/hold", nil)
    	req.Header.Set("X-API-Key", os.Getenv("ORBIT_API_KEY"))
    	http.DefaultClient.Do(req)
    }
    ```

    ```php PHP theme={null}
    <?php
    $ch = curl_init('https://api.orbit.devotel.io/api/v1/voice/calls/call_abc123/hold');
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST');
    curl_setopt($ch, CURLOPT_HTTPHEADER, ['X-API-Key: ' . getenv('ORBIT_API_KEY')]);
    echo curl_exec($ch);
    ```
  </CodeGroup>
</RequestExample>

### Send DTMF Tones

`POST /api/v1/voice/calls/{id}/dtmf`

<ParamField body="digits" type="string" required>
  String of DTMF digits to send (`0`–`9`, `*`, `#`, `A`–`D`), up to 32 characters. Each digit plays for `duration_ms` followed by a `gap_ms` silence — by default 200ms tone / 50ms gap.
</ParamField>

<ParamField body="duration_ms" type="integer" default="200">
  Per-digit tone duration in milliseconds. Range `50`–`1000`.
</ParamField>

<ParamField body="gap_ms" type="integer" default="50">
  Inter-digit gap in milliseconds. Range `50`–`500`.
</ParamField>

Use to navigate carrier IVRs from a programmatic outbound call (PIN entry, account confirmations, etc.).

<RequestExample>
  <CodeGroup>
    ```bash cURL theme={null}
    curl -X POST "https://api.orbit.devotel.io/api/v1/voice/calls/call_abc123/dtmf" \
      -H "X-API-Key: dv_live_sk_your_key_here" \
      -H "Content-Type: application/json" \
      -d '{ "digits": "1234#" }'
    ```

    ```typescript Node.js theme={null}
    await orbit.voice.calls.sendDTMF('call_abc123', { digits: '1234#' })
    ```

    ```python Python theme={null}
    import os, requests
    headers = {"X-API-Key": os.environ["ORBIT_API_KEY"], "Content-Type": "application/json"}
    r = requests.post("https://api.orbit.devotel.io/api/v1/voice/calls/call_abc123/dtmf",
                      headers=headers, json={"digits": "1234#"})
    print(r.json())
    ```

    ```go Go theme={null}
    package main

    import (
    	"bytes"
    	"net/http"
    	"os"
    )

    func main() {
    	req, _ := http.NewRequest("POST", "https://api.orbit.devotel.io/api/v1/voice/calls/call_abc123/dtmf",
    		bytes.NewBuffer([]byte(`{"digits":"1234#"}`)))
    	req.Header.Set("X-API-Key", os.Getenv("ORBIT_API_KEY"))
    	req.Header.Set("Content-Type", "application/json")
    	http.DefaultClient.Do(req)
    }
    ```

    ```php PHP theme={null}
    <?php
    $ch = curl_init('https://api.orbit.devotel.io/api/v1/voice/calls/call_abc123/dtmf');
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST');
    curl_setopt($ch, CURLOPT_HTTPHEADER, [
      'X-API-Key: ' . getenv('ORBIT_API_KEY'),
      'Content-Type: application/json',
    ]);
    curl_setopt($ch, CURLOPT_POSTFIELDS, '{"digits":"1234#"}');
    echo curl_exec($ch);
    ```
  </CodeGroup>
</RequestExample>

### Transfer (Blind)

`POST /api/v1/voice/calls/{id}/transfer`

Cold-transfer the call to a new destination. Hangs up the original leg the moment the new leg connects.

<ParamField body="to" type="string" required>
  Destination phone number, agent ID (`agt_*`), or extension (`ext_*`).
</ParamField>

<RequestExample>
  <CodeGroup>
    ```bash cURL theme={null}
    curl -X POST "https://api.orbit.devotel.io/api/v1/voice/calls/call_abc123/transfer" \
      -H "X-API-Key: dv_live_sk_your_key_here" \
      -H "Content-Type: application/json" \
      -d '{ "to": "+14155557890" }'
    ```

    ```typescript Node.js theme={null}
    await orbit.voice.calls.transfer('call_abc123', {
      to: '+14155557890',
    })
    ```

    ```python Python theme={null}
    import os, requests
    headers = {"X-API-Key": os.environ["ORBIT_API_KEY"], "Content-Type": "application/json"}
    r = requests.post("https://api.orbit.devotel.io/api/v1/voice/calls/call_abc123/transfer",
                      headers=headers, json={"to": "+14155557890"})
    print(r.json())
    ```

    ```go Go theme={null}
    package main

    import (
    	"bytes"
    	"net/http"
    	"os"
    )

    func main() {
    	req, _ := http.NewRequest("POST", "https://api.orbit.devotel.io/api/v1/voice/calls/call_abc123/transfer",
    		bytes.NewBuffer([]byte(`{"to":"+14155557890"}`)))
    	req.Header.Set("X-API-Key", os.Getenv("ORBIT_API_KEY"))
    	req.Header.Set("Content-Type", "application/json")
    	http.DefaultClient.Do(req)
    }
    ```

    ```php PHP theme={null}
    <?php
    $ch = curl_init('https://api.orbit.devotel.io/api/v1/voice/calls/call_abc123/transfer');
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST');
    curl_setopt($ch, CURLOPT_HTTPHEADER, [
      'X-API-Key: ' . getenv('ORBIT_API_KEY'),
      'Content-Type: application/json',
    ]);
    curl_setopt($ch, CURLOPT_POSTFIELDS, '{"to":"+14155557890"}');
    echo curl_exec($ch);
    ```
  </CodeGroup>
</RequestExample>

### Warm Transfer

`POST /api/v1/voice/calls/{id}/warm-transfer`

Start a warm transfer — the calling agent stays connected while a third leg is dialled. The original caller is placed on hold; once the third party answers, the agent has a private "whisper" window with them before completing the handoff.

<ParamField body="destination" type="string">
  E.164 destination for the warm-transfer consult leg (e.g. `+14155557890`). Mutually exclusive with `queueId` — supply exactly one.
</ParamField>

<ParamField body="queueId" type="string">
  Queue to transfer into instead of a direct number (attended-transfer-to-queue). Mutually exclusive with `destination` — supply exactly one. Request is rejected when both or neither are present.
</ParamField>

<ParamField body="contextWhisper" type="boolean" default="false">
  When `true`, speak a context sentence to the consult leg on answer before audio bridges.
</ParamField>

<ParamField body="context" type="string">
  Custom whisper text to speak to the consult leg (max 220 characters), e.g. "Customer Jane Doe, escalation tier 2". When omitted and `contextWhisper` is `true`, a context sentence is generated automatically.
</ParamField>

<ParamField body="from" type="string">
  Caller ID (E.164) to present on the consult leg. Must be a number your organization owns — a number you do not own is rejected. When omitted, the platform default caller ID is used.
</ParamField>

<ParamField body="consultFirst" type="boolean" default="true">
  When `true` (the default), the agent gets a private consult window with the third party before the handoff can be completed. Set to `false` to bridge the caller in as soon as the consult leg answers.
</ParamField>

<ParamField body="transfer_reason" type="string">
  Optional reason for the transfer, recorded on the call's transfer history for transfer-rate analytics. One of `wrong-queue`, `escalation`, `language-mismatch`, `expertise`, or `other`.
</ParamField>

<ParamField body="priority" type="integer">
  Queue transfers only (used with `queueId`). Routing priority from `0` to `99` for the agent match — lower wins. Ignored when transferring to a `destination`.
</ParamField>

<ParamField body="requiredSkills" type="string[]">
  Queue transfers only (used with `queueId`). Skill tags (up to 20) the matched agent must have — only agents with every listed skill are eligible. Ignored when transferring to a `destination`.
</ParamField>

`POST /api/v1/voice/calls/{id}/warm-transfer/complete`

Bridge the original caller into the consult leg and disconnect the original agent. The two non-agent legs are now connected.

`POST /api/v1/voice/calls/{id}/warm-transfer/cancel`

Abort the warm transfer and return the original caller to the agent. The consult leg is hung up.

<RequestExample>
  <CodeGroup>
    ```bash cURL theme={null}
    curl -X POST "https://api.orbit.devotel.io/api/v1/voice/calls/call_abc123/warm-transfer" \
      -H "X-API-Key: dv_live_sk_your_key_here" \
      -H "Content-Type: application/json" \
      -d '{ "destination": "+14155557890", "contextWhisper": true, "context": "Customer Jane Doe, escalation tier 2" }'
    ```

    ```typescript Node.js theme={null}
    await orbit.voice.calls.warmTransfer('call_abc123', {
      destination: '+14155557890',
      contextWhisper: true,
      context: 'Customer Jane Doe, escalation tier 2',
    })
    // later:
    await orbit.voice.calls.warmTransferComplete('call_abc123')
    // or to abort:
    await orbit.voice.calls.warmTransferCancel('call_abc123')
    ```

    ```python Python theme={null}
    import os, requests
    headers = {"X-API-Key": os.environ["ORBIT_API_KEY"], "Content-Type": "application/json"}
    r = requests.post("https://api.orbit.devotel.io/api/v1/voice/calls/call_abc123/warm-transfer",
                      headers=headers,
                      json={"destination": "+14155557890", "contextWhisper": True, "context": "Customer Jane Doe, escalation tier 2"})
    print(r.json())
    ```

    ```go Go theme={null}
    package main

    import (
    	"bytes"
    	"net/http"
    	"os"
    )

    func main() {
    	req, _ := http.NewRequest("POST", "https://api.orbit.devotel.io/api/v1/voice/calls/call_abc123/warm-transfer",
    		bytes.NewBuffer([]byte(`{"destination":"+14155557890","contextWhisper":true,"context":"Customer Jane Doe, escalation tier 2"}`)))
    	req.Header.Set("X-API-Key", os.Getenv("ORBIT_API_KEY"))
    	req.Header.Set("Content-Type", "application/json")
    	http.DefaultClient.Do(req)
    }
    ```

    ```php PHP theme={null}
    <?php
    $ch = curl_init('https://api.orbit.devotel.io/api/v1/voice/calls/call_abc123/warm-transfer');
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST');
    curl_setopt($ch, CURLOPT_HTTPHEADER, [
      'X-API-Key: ' . getenv('ORBIT_API_KEY'),
      'Content-Type: application/json',
    ]);
    curl_setopt($ch, CURLOPT_POSTFIELDS,
      '{"destination":"+14155557890","contextWhisper":true,"context":"Customer Jane Doe, escalation tier 2"}');
    echo curl_exec($ch);
    ```
  </CodeGroup>
</RequestExample>

***

## Recording

Recordings are configured per-call via `record: true` on `POST /api/v1/voice/calls`, or started/stopped mid-call via the endpoints below. Storage is tenant-isolated; URLs expire after 1 hour and require re-fetching via the call detail endpoint.

### Start / Stop Mid-Call Recording

`POST /api/v1/voice/calls/{id}/recording/start`

Begin recording an in-progress call. Idempotent — calling on an already-recording call returns the existing recording id. This endpoint takes no body parameters; channel layout and silence trimming are determined by the call's recording configuration.

`POST /api/v1/voice/calls/{id}/recording/stop`

Stop the in-progress recording and finalise the file. Subsequent reads of `GET /api/v1/voice/calls/{id}/recording` will return the fully-encoded URL.

<RequestExample>
  <CodeGroup>
    ```bash cURL theme={null}
    curl -X POST "https://api.orbit.devotel.io/api/v1/voice/calls/call_abc123/recording/start" \
      -H "X-API-Key: dv_live_sk_your_key_here"
    ```

    ```typescript Node.js theme={null}
    await orbit.voice.recordings.start('call_abc123')
    await orbit.voice.recordings.stop('call_abc123')
    ```

    ```python Python theme={null}
    import os, requests
    headers = {"X-API-Key": os.environ["ORBIT_API_KEY"]}
    r = requests.post("https://api.orbit.devotel.io/api/v1/voice/calls/call_abc123/recording/start",
                      headers=headers)
    print(r.json())
    ```

    ```go Go theme={null}
    package main

    import (
    	"net/http"
    	"os"
    )

    func main() {
    	req, _ := http.NewRequest("POST", "https://api.orbit.devotel.io/api/v1/voice/calls/call_abc123/recording/start", nil)
    	req.Header.Set("X-API-Key", os.Getenv("ORBIT_API_KEY"))
    	http.DefaultClient.Do(req)
    }
    ```

    ```php PHP theme={null}
    <?php
    $ch = curl_init('https://api.orbit.devotel.io/api/v1/voice/calls/call_abc123/recording/start');
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST');
    curl_setopt($ch, CURLOPT_HTTPHEADER, [
      'X-API-Key: ' . getenv('ORBIT_API_KEY'),
    ]);
    echo curl_exec($ch);
    ```
  </CodeGroup>
</RequestExample>

### Retrieve a Call Recording

`GET /api/v1/voice/calls/{id}/recording`

Returns the recording metadata + a 1-hour-expiring signed URL. If the call is still being recorded, `status` is `in_progress` and `signed_url` is null.

<RequestExample>
  <CodeGroup>
    ```bash cURL theme={null}
    curl -X GET "https://api.orbit.devotel.io/api/v1/voice/calls/call_abc123/recording" \
      -H "X-API-Key: dv_live_sk_your_key_here"
    ```

    ```typescript Node.js theme={null}
    const rec = await orbit.voice.recordings.get('call_abc123')
    console.log(rec.data.signed_url)
    ```

    ```python Python theme={null}
    import os, requests
    headers = {"X-API-Key": os.environ["ORBIT_API_KEY"]}
    r = requests.get("https://api.orbit.devotel.io/api/v1/voice/calls/call_abc123/recording", headers=headers)
    print(r.json())
    ```

    ```go Go theme={null}
    package main

    import (
    	"net/http"
    	"os"
    )

    func main() {
    	req, _ := http.NewRequest("GET", "https://api.orbit.devotel.io/api/v1/voice/calls/call_abc123/recording", nil)
    	req.Header.Set("X-API-Key", os.Getenv("ORBIT_API_KEY"))
    	http.DefaultClient.Do(req)
    }
    ```

    ```php PHP theme={null}
    <?php
    $ch = curl_init('https://api.orbit.devotel.io/api/v1/voice/calls/call_abc123/recording');
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($ch, CURLOPT_HTTPHEADER, ['X-API-Key: ' . getenv('ORBIT_API_KEY')]);
    echo curl_exec($ch);
    ```
  </CodeGroup>
</RequestExample>

<ResponseExample>
  ```json 200 theme={null}
  {
    "data": {
      "call_id": "call_abc123",
      "status": "completed",
      "duration_seconds": 312,
      "format": "mp3",
      "channels": 2,
      "size_bytes": 4980000,
      "signed_url": "https://storage.devotel.io/recordings/call_abc123.mp3?Expires=1735689600&Signature=...",
      "signed_url_expires_at": "2026-03-09T12:00:00Z",
      "created_at": "2026-03-08T11:55:48Z"
    },
    "meta": {
      "request_id": "req_rec_001",
      "timestamp": "2026-03-08T12:00:00Z"
    }
  }
  ```
</ResponseExample>

### Conference Recording

`POST /api/v1/voice/conferences/{id}/recording/start`

`POST /api/v1/voice/conferences/{id}/recording/stop`

Same semantics as call recording but for conference rooms — captures the mixed audio of every participant.

<RequestExample>
  <CodeGroup>
    ```bash cURL theme={null}
    curl -X POST "https://api.orbit.devotel.io/api/v1/voice/conferences/conf_abc123/recording/start" \
      -H "X-API-Key: dv_live_sk_your_key_here"
    ```

    ```typescript Node.js theme={null}
    await orbit.voice.conferences.startRecording('conf_abc123')
    await orbit.voice.conferences.stopRecording('conf_abc123')
    ```

    ```python Python theme={null}
    import os, requests
    headers = {"X-API-Key": os.environ["ORBIT_API_KEY"]}
    r = requests.post("https://api.orbit.devotel.io/api/v1/voice/conferences/conf_abc123/recording/start", headers=headers)
    print(r.json())
    ```

    ```go Go theme={null}
    package main

    import (
    	"net/http"
    	"os"
    )

    func main() {
    	req, _ := http.NewRequest("POST",
    		"https://api.orbit.devotel.io/api/v1/voice/conferences/conf_abc123/recording/start", nil)
    	req.Header.Set("X-API-Key", os.Getenv("ORBIT_API_KEY"))
    	http.DefaultClient.Do(req)
    }
    ```

    ```php PHP theme={null}
    <?php
    $ch = curl_init('https://api.orbit.devotel.io/api/v1/voice/conferences/conf_abc123/recording/start');
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST');
    curl_setopt($ch, CURLOPT_HTTPHEADER, ['X-API-Key: ' . getenv('ORBIT_API_KEY')]);
    echo curl_exec($ch);
    ```
  </CodeGroup>
</RequestExample>

***

## SIP Trunks

Bring your own carrier by connecting SIP trunks to Orbit. Orbit acts as an SBC (Session Border Controller) with TLS and SRTP encryption.

### Create SIP Trunk

`POST /api/v1/voice/sip-trunks`

<ParamField body="name" type="string" required>
  A descriptive name for the trunk (1-120 characters)
</ParamField>

<ParamField body="host" type="string" required>
  Hostname or IP address of the SIP trunk endpoint (1-253 characters)
</ParamField>

<ParamField body="port" type="number" default="5060">
  SIP port (1-65535). Defaults to 5060 if omitted.
</ParamField>

<ParamField body="transport" type="string" default="udp">
  Transport protocol: `udp`, `tcp`, or `tls`. Defaults to `udp` if omitted.
</ParamField>

<ParamField body="authentication" type="object">
  Optional SIP authentication credentials

  <Expandable title="Properties">
    <ResponseField name="username" type="string">
      SIP authentication username (max 128 characters)
    </ResponseField>

    <ResponseField name="password" type="string">
      SIP authentication password (max 256 characters)
    </ResponseField>
  </Expandable>
</ParamField>

<ParamField body="codecs" type="string[]">
  Preferred audio codecs in priority order (max 20 codecs, each ≤32 characters). When omitted, no codec preference is stored and the carrier's negotiated default applies.
</ParamField>

<ParamField body="enabled" type="boolean" default="true">
  Whether the trunk is enabled for outbound calling. Defaults to `true` if omitted.
</ParamField>

<ParamField body="maxConcurrent" type="number">
  Maximum concurrent calls on this trunk (1-2000)
</ParamField>

<RequestExample>
  <CodeGroup>
    ```bash cURL 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": "Primary Carrier",
      "host": "trunk.carrier.com",
      "port": 5060,
      "transport": "tls",
      "authentication": {
        "username": "orbit_user",
        "password": "secure_password"
      },
      "codecs": ["OPUS", "PCMU"],
      "enabled": true,
      "maxConcurrent": 50
    }'
    ```

    ```typescript Node.js theme={null}
    import { Orbit } from '@devotel/orbit-sdk'

    const orbit = new Orbit({ apiKey: process.env.ORBIT_API_KEY! })

    const trunk = await orbit.voice.sipTrunks.create({
      name: 'Primary Carrier',
      host: 'trunk.carrier.com',
      port: 5060,
      transport: 'tls',
      authentication: {
        username: 'orbit_user',
        password: 'secure_password',
      },
      codecs: ['OPUS', 'PCMU'],
      enabled: true,
      maxConcurrent: 50,
    })
    console.log(trunk.data.id)
    ```

    ```python Python theme={null}
    import os, requests
    headers = {"X-API-Key": os.environ["ORBIT_API_KEY"], "Content-Type": "application/json"}
    r = requests.post("https://api.orbit.devotel.io/api/v1/voice/sip-trunks", headers=headers, json={
      "name": "Primary Carrier",
      "host": "trunk.carrier.com",
      "port": 5060,
      "transport": "tls",
      "authentication": {
        "username": "orbit_user",
        "password": "secure_password"
      },
      "codecs": ["OPUS", "PCMU"],
      "enabled": True,
      "maxConcurrent": 50
    })
    print(r.json())
    ```

    ```go Go theme={null}
    package main

    import (
    	"bytes"
    	"net/http"
    	"os"
    )

    func main() {
    	req, _ := http.NewRequest("POST", "https://api.orbit.devotel.io/api/v1/voice/sip-trunks", bytes.NewBuffer([]byte(`{
      "name": "Primary Carrier",
      "host": "trunk.carrier.com",
      "port": 5060,
      "transport": "tls",
      "authentication": {
        "username": "orbit_user",
        "password": "secure_password"
      },
      "codecs": ["OPUS", "PCMU"],
      "enabled": true,
      "maxConcurrent": 50
    }`)))
    	req.Header.Set("X-API-Key", os.Getenv("ORBIT_API_KEY"))
    	req.Header.Set("Content-Type", "application/json")
    	http.DefaultClient.Do(req)
    }
    ```

    ```php PHP theme={null}
    <?php
    $ch = curl_init('https://api.orbit.devotel.io/api/v1/voice/sip-trunks');
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST');
    curl_setopt($ch, CURLOPT_HTTPHEADER, [
      'X-API-Key: ' . getenv('ORBIT_API_KEY'),
      'Content-Type: application/json',
    ]);
    curl_setopt($ch, CURLOPT_POSTFIELDS, <<<JSON
    {
      "name": "Primary Carrier",
      "host": "trunk.carrier.com",
      "port": 5060,
      "transport": "tls",
      "authentication": {
        "username": "orbit_user",
        "password": "secure_password"
      },
      "codecs": ["OPUS", "PCMU"],
      "enabled": true,
      "maxConcurrent": 50
    }
    JSON);
    echo curl_exec($ch);
    ```
  </CodeGroup>
</RequestExample>

<ResponseExample>
  ```json 201 theme={null}
  {
    "data": {
      "id": "trunk_abc123",
      "name": "Primary Carrier",
      "host": "trunk.carrier.com",
      "port": 5060,
      "transport": "tls",
      "authentication": {
        "username": "orbit_user"
      },
      "codecs": ["OPUS", "PCMU"],
      "enabled": true,
      "created_at": "2026-03-08T12:00:00Z"
    },
    "meta": {
      "request_id": "req_trunk_001",
      "timestamp": "2026-03-08T12:00:00Z"
    }
  }
  ```
</ResponseExample>

### List SIP Trunks

`GET /api/v1/voice/sip-trunks`

### Get SIP Trunk

`GET /api/v1/voice/sip-trunks/{id}`

### Update SIP Trunk

`PUT /api/v1/voice/sip-trunks/{id}`

### Delete SIP Trunk

`DELETE /api/v1/voice/sip-trunks/{id}`

<RequestExample>
  <CodeGroup>
    ```bash cURL theme={null}
    # List
    curl -X GET "https://api.orbit.devotel.io/api/v1/voice/sip-trunks" \
      -H "X-API-Key: dv_live_sk_your_key_here"
    # Get
    curl -X GET "https://api.orbit.devotel.io/api/v1/voice/sip-trunks/trunk_abc123" \
      -H "X-API-Key: dv_live_sk_your_key_here"
    # Update
    curl -X PUT "https://api.orbit.devotel.io/api/v1/voice/sip-trunks/trunk_abc123" \
      -H "X-API-Key: dv_live_sk_your_key_here" \
      -H "Content-Type: application/json" \
      -d '{ "name": "Primary Carrier (renamed)" }'
    # Delete
    curl -X DELETE "https://api.orbit.devotel.io/api/v1/voice/sip-trunks/trunk_abc123" \
      -H "X-API-Key: dv_live_sk_your_key_here"
    ```

    ```typescript Node.js theme={null}
    await orbit.voice.sipTrunks.list()
    await orbit.voice.sipTrunks.get('trunk_abc123')
    await orbit.voice.sipTrunks.update('trunk_abc123', { name: 'Primary Carrier (renamed)' })
    await orbit.voice.sipTrunks.delete('trunk_abc123')
    ```

    ```python Python theme={null}
    import os, requests
    h = {"X-API-Key": os.environ["ORBIT_API_KEY"]}
    requests.get("https://api.orbit.devotel.io/api/v1/voice/sip-trunks", headers=h)
    requests.get("https://api.orbit.devotel.io/api/v1/voice/sip-trunks/trunk_abc123", headers=h)
    requests.put("https://api.orbit.devotel.io/api/v1/voice/sip-trunks/trunk_abc123",
                 headers={**h, "Content-Type": "application/json"},
                 json={"name": "Primary Carrier (renamed)"})
    requests.delete("https://api.orbit.devotel.io/api/v1/voice/sip-trunks/trunk_abc123", headers=h)
    ```

    ```go Go theme={null}
    package main

    import (
    	"net/http"
    	"os"
    )

    func main() {
    	base := "https://api.orbit.devotel.io/api/v1/voice/sip-trunks"
    	key := os.Getenv("ORBIT_API_KEY")
    	for _, r := range []struct{ m, u string }{{"GET", base}, {"GET", base + "/trunk_abc123"}, {"DELETE", base + "/trunk_abc123"}} {
    		req, _ := http.NewRequest(r.m, r.u, nil)
    		req.Header.Set("X-API-Key", key)
    		http.DefaultClient.Do(req)
    	}
    }
    ```

    ```php PHP theme={null}
    <?php
    $key = getenv('ORBIT_API_KEY');
    $base = 'https://api.orbit.devotel.io/api/v1/voice/sip-trunks';
    foreach (['GET' => $base, 'DELETE' => "$base/trunk_abc123"] as $method => $url) {
      $ch = curl_init($url);
      curl_setopt_array($ch, [
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_CUSTOMREQUEST => $method,
        CURLOPT_HTTPHEADER => ["X-API-Key: $key"],
      ]);
      echo curl_exec($ch);
    }
    ```
  </CodeGroup>
</RequestExample>

***

## IVR (Interactive Voice Response)

Build programmable voice menus that route callers through options, collect DTMF input, and transfer to agents or queues.

### Create IVR Flow

`POST /api/v1/voice/ivr-flows`

<Note>
  The IVR flow body is a **graph definition**, not a flat list of greeting/menu
  fields. The greeting, per-option prompts, timeouts, and fallbacks are all
  expressed as nodes and edges inside the required `definition` object. The API
  ignores any top-level field other than `name`, `definition`, and `active`.
</Note>

<ParamField body="name" type="string" required>
  IVR flow name (1–200 characters).
</ParamField>

<ParamField body="definition" type="object" required>
  The IVR flow graph, expressed as an [XYFlow](https://reactflow.dev/)-style
  `{ nodes, edges }` object (the same shape the dashboard IVR builder canvas
  produces). It is structurally validated before persistence — an invalid graph
  is rejected with `422 IVR_FLOW_GRAPH_INVALID` and a list of node/edge errors.

  Validation rules: exactly one entry node (type `ivrStart`, or any node with
  `data.isEntry = true`); at least one exit node (`end`, `hangup`, `transfer`,
  `voicemail`, `ringGroup`, `dialByName`, or `offerCallback`); every edge must
  reference existing nodes; every node must be reachable from the entry; and
  every reachable node must have a path to an exit (a loop with no escape is
  rejected to avoid burning carrier minutes). Limits: 1000 nodes, 2000 edges.

  <Expandable title="definition">
    <ParamField body="definition.nodes" type="object[]" required>
      Flow nodes.

      <Expandable title="node">
        <ParamField body="definition.nodes[].id" type="string" required>
          Unique node id (referenced by edges).
        </ParamField>

        <ParamField body="definition.nodes[].type" type="string" required>
          Node type. Entry: `ivrStart`. Interactive: `menu`. Terminal/exit:
          `transfer`, `voicemail`, `hangup`, `end`, `ringGroup`, `dialByName`,
          `offerCallback`.
        </ParamField>

        <ParamField body="definition.nodes[].data" type="object">
          Per-node config (camelCase). For a `menu` node: `ttsText` (the spoken
          menu prompt), optional `language`, `timeout` (seconds), and
          `menuOptions` — an array of `{ key, label }` where `key` is a DTMF key
          (`0`–`9`, `*`, `#`). For a `transfer` node: `transferTo` — a PSTN
          number (E.164) or on-net extension. Per invariant #45, outbound voice
          must exit via the Devotel softswitch, so a free-form `sip:`/`sips:`
          `transferTo` is rejected with `transfer_external_sip`.
        </ParamField>
      </Expandable>
    </ParamField>

    <ParamField body="definition.edges" type="object[]" required>
      Directed edges connecting nodes.

      <Expandable title="edge">
        <ParamField body="definition.edges[].source" type="string" required>
          Source node id.
        </ParamField>

        <ParamField body="definition.edges[].target" type="string" required>
          Target node id.
        </ParamField>

        <ParamField body="definition.edges[].sourceHandle" type="string">
          For edges leaving a `menu` node, the DTMF key this branch is taken on
          (e.g. `"1"`). Matched against `menuOptions[].key`.
        </ParamField>

        <ParamField body="definition.edges[].id" type="string">
          Optional edge id.
        </ParamField>
      </Expandable>
    </ParamField>
  </Expandable>
</ParamField>

<ParamField body="active" type="boolean" default="true">
  Whether the flow is active.
</ParamField>

<RequestExample>
  <CodeGroup>
    ```bash cURL theme={null}
    curl -X POST "https://api.orbit.devotel.io/api/v1/voice/ivr-flows" \
      -H "X-API-Key: dv_live_sk_your_key_here" \
      -H "Content-Type: application/json" \
      -d '{
      "name": "Main Menu",
      "active": true,
      "definition": {
        "nodes": [
          { "id": "start", "type": "ivrStart", "data": {} },
          { "id": "menu", "type": "menu", "data": {
              "ttsText": "Welcome to Acme Corp. Press 1 for sales, 2 for support, or 3 to leave a voicemail.",
              "language": "en-US",
              "timeout": 8,
              "menuOptions": [
                { "key": "1", "label": "Sales" },
                { "key": "2", "label": "Support" },
                { "key": "3", "label": "Voicemail" }
              ]
            }
          },
          { "id": "sales", "type": "transfer", "data": { "transferTo": "+14155550101" } },
          { "id": "support", "type": "transfer", "data": { "transferTo": "+14155550102" } },
          { "id": "voicemail", "type": "voicemail", "data": { "greeting": "Please leave a message after the tone." } }
        ],
        "edges": [
          { "id": "e0", "source": "start", "target": "menu" },
          { "id": "e1", "source": "menu", "target": "sales", "sourceHandle": "1" },
          { "id": "e2", "source": "menu", "target": "support", "sourceHandle": "2" },
          { "id": "e3", "source": "menu", "target": "voicemail", "sourceHandle": "3" }
        ]
      }
    }'
    ```

    ```typescript Node.js theme={null}
    const ivr = await orbit.voice.ivr.create({
      name: 'Main Menu',
      active: true,
      definition: {
        nodes: [
          { id: 'start', type: 'ivrStart', data: {} },
          {
            id: 'menu',
            type: 'menu',
            data: {
              ttsText: 'Welcome to Acme Corp. Press 1 for sales, 2 for support, or 3 to leave a voicemail.',
              language: 'en-US',
              timeout: 8,
              menuOptions: [
                { key: '1', label: 'Sales' },
                { key: '2', label: 'Support' },
                { key: '3', label: 'Voicemail' },
              ],
            },
          },
          { id: 'sales', type: 'transfer', data: { transferTo: '+14155550101' } },
          { id: 'support', type: 'transfer', data: { transferTo: '+14155550102' } },
          { id: 'voicemail', type: 'voicemail', data: { greeting: 'Please leave a message after the tone.' } },
        ],
        edges: [
          { id: 'e0', source: 'start', target: 'menu' },
          { id: 'e1', source: 'menu', target: 'sales', sourceHandle: '1' },
          { id: 'e2', source: 'menu', target: 'support', sourceHandle: '2' },
          { id: 'e3', source: 'menu', target: 'voicemail', sourceHandle: '3' },
        ],
      },
    })
    ```

    ```python Python theme={null}
    import os, requests
    headers = {"X-API-Key": os.environ["ORBIT_API_KEY"], "Content-Type": "application/json"}
    r = requests.post("https://api.orbit.devotel.io/api/v1/voice/ivr-flows", headers=headers, json={
      "name": "Main Menu",
      "active": True,
      "definition": {
        "nodes": [
          {"id": "start", "type": "ivrStart", "data": {}},
          {"id": "menu", "type": "menu", "data": {
            "ttsText": "Welcome to Acme Corp. Press 1 for sales, 2 for support, or 3 to leave a voicemail.",
            "language": "en-US",
            "timeout": 8,
            "menuOptions": [
              {"key": "1", "label": "Sales"},
              {"key": "2", "label": "Support"},
              {"key": "3", "label": "Voicemail"}
            ]
          }},
          {"id": "sales", "type": "transfer", "data": {"transferTo": "+14155550101"}},
          {"id": "support", "type": "transfer", "data": {"transferTo": "+14155550102"}},
          {"id": "voicemail", "type": "voicemail", "data": {"greeting": "Please leave a message after the tone."}}
        ],
        "edges": [
          {"id": "e0", "source": "start", "target": "menu"},
          {"id": "e1", "source": "menu", "target": "sales", "sourceHandle": "1"},
          {"id": "e2", "source": "menu", "target": "support", "sourceHandle": "2"},
          {"id": "e3", "source": "menu", "target": "voicemail", "sourceHandle": "3"}
        ]
      }
    })
    print(r.json())
    ```

    ```go Go theme={null}
    package main

    import (
    	"bytes"
    	"net/http"
    	"os"
    )

    func main() {
    	body := []byte(`{
      "name": "Main Menu",
      "active": true,
      "definition": {
        "nodes": [
          {"id": "start", "type": "ivrStart", "data": {}},
          {"id": "menu", "type": "menu", "data": {
            "ttsText": "Welcome to Acme Corp. Press 1 for sales.",
            "timeout": 8,
            "menuOptions": [{"key": "1", "label": "Sales"}]
          }},
          {"id": "sales", "type": "transfer", "data": {"transferTo": "+14155550101"}}
        ],
        "edges": [
          {"id": "e0", "source": "start", "target": "menu"},
          {"id": "e1", "source": "menu", "target": "sales", "sourceHandle": "1"}
        ]
      }
    }`)
    	req, _ := http.NewRequest("POST", "https://api.orbit.devotel.io/api/v1/voice/ivr-flows", bytes.NewBuffer(body))
    	req.Header.Set("X-API-Key", os.Getenv("ORBIT_API_KEY"))
    	req.Header.Set("Content-Type", "application/json")
    	http.DefaultClient.Do(req)
    }
    ```

    ```php PHP theme={null}
    <?php
    $ch = curl_init('https://api.orbit.devotel.io/api/v1/voice/ivr-flows');
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST');
    curl_setopt($ch, CURLOPT_HTTPHEADER, [
      'X-API-Key: ' . getenv('ORBIT_API_KEY'),
      'Content-Type: application/json',
    ]);
    curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode([
      'name' => 'Main Menu',
      'active' => true,
      'definition' => [
        'nodes' => [
          ['id' => 'start', 'type' => 'ivrStart', 'data' => (object) []],
          ['id' => 'menu', 'type' => 'menu', 'data' => [
            'ttsText' => 'Welcome to Acme Corp. Press 1 for sales.',
            'timeout' => 8,
            'menuOptions' => [['key' => '1', 'label' => 'Sales']],
          ]],
          ['id' => 'sales', 'type' => 'transfer', 'data' => ['transferTo' => '+14155550101']],
        ],
        'edges' => [
          ['id' => 'e0', 'source' => 'start', 'target' => 'menu'],
          ['id' => 'e1', 'source' => 'menu', 'target' => 'sales', 'sourceHandle' => '1'],
        ],
      ],
    ]));
    echo curl_exec($ch);
    ```
  </CodeGroup>
</RequestExample>

<ResponseExample>
  ```json 201 theme={null}
  {
    "data": {
      "id": "ivr_abc123",
      "name": "Main Menu",
      "status": "active",
      "options_count": 3,
      "created_at": "2026-03-08T12:00:00Z"
    },
    "meta": {
      "request_id": "req_ivr_001",
      "timestamp": "2026-03-08T12:00:00Z"
    }
  }
  ```
</ResponseExample>

### List IVR Flows

`GET /api/v1/voice/ivr-flows`

### Update IVR Flow

`PUT /api/v1/voice/ivr-flows/{id}`

### Delete IVR Flow

`DELETE /api/v1/voice/ivr-flows/{id}`

<RequestExample>
  <CodeGroup>
    ```bash cURL theme={null}
    # List
    curl -X GET "https://api.orbit.devotel.io/api/v1/voice/ivr-flows" \
      -H "X-API-Key: dv_live_sk_your_key_here"
    # Update
    curl -X PUT "https://api.orbit.devotel.io/api/v1/voice/ivr-flows/ivr_abc123" \
      -H "X-API-Key: dv_live_sk_your_key_here" \
      -H "Content-Type: application/json" \
      -d '{ "name": "Renamed Menu" }'
    # Delete
    curl -X DELETE "https://api.orbit.devotel.io/api/v1/voice/ivr-flows/ivr_abc123" \
      -H "X-API-Key: dv_live_sk_your_key_here"
    ```

    ```typescript Node.js theme={null}
    await orbit.voice.ivr.list()
    await orbit.voice.ivr.update('ivr_abc123', { name: 'Renamed Menu' })
    await orbit.voice.ivr.delete('ivr_abc123')
    ```

    ```python Python theme={null}
    import os, requests
    h = {"X-API-Key": os.environ["ORBIT_API_KEY"]}
    requests.get("https://api.orbit.devotel.io/api/v1/voice/ivr-flows", headers=h)
    requests.put("https://api.orbit.devotel.io/api/v1/voice/ivr-flows/ivr_abc123",
                 headers={**h, "Content-Type": "application/json"}, json={"name": "Renamed Menu"})
    requests.delete("https://api.orbit.devotel.io/api/v1/voice/ivr-flows/ivr_abc123", headers=h)
    ```

    ```go Go theme={null}
    package main

    import (
    	"net/http"
    	"os"
    )

    func main() {
    	req, _ := http.NewRequest("GET", "https://api.orbit.devotel.io/api/v1/voice/ivr-flows", nil)
    	req.Header.Set("X-API-Key", os.Getenv("ORBIT_API_KEY"))
    	http.DefaultClient.Do(req)
    }
    ```

    ```php PHP theme={null}
    <?php
    $ch = curl_init('https://api.orbit.devotel.io/api/v1/voice/ivr-flows');
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($ch, CURLOPT_HTTPHEADER, ['X-API-Key: ' . getenv('ORBIT_API_KEY')]);
    echo curl_exec($ch);
    ```
  </CodeGroup>
</RequestExample>

***

## Conferences

Host multi-party conference calls with moderation controls, recording, and real-time participant management.

### List Conferences

`GET /api/v1/voice/conferences`

Lists conferences for your tenant, paginated by `started_at` descending.

<ParamField query="status" type="string">
  Comma-separated status filter — `pending`, `in-progress`, `completed`, `failed`, `cancelled`.
</ParamField>

<ParamField query="limit" type="integer" default="50">
  Page size (max 100).
</ParamField>

<ParamField query="cursor" type="string">
  Opaque pagination cursor returned in the previous response's `meta.cursor`.
</ParamField>

<RequestExample>
  <CodeGroup>
    ```bash cURL theme={null}
    curl -X GET "https://api.orbit.devotel.io/api/v1/voice/conferences?status=in-progress&limit=20" \
      -H "X-API-Key: dv_live_sk_your_key_here"
    ```

    ```typescript Node.js theme={null}
    const conferences = await orbit.voice.conferences.list({
      status: 'in-progress',
      limit: 20,
    })
    for (const conf of conferences.data) {
      console.log(conf.id, conf.name, conf.status)
    }
    ```

    ```python Python theme={null}
    import os, requests
    headers = {"X-API-Key": os.environ["ORBIT_API_KEY"]}
    r = requests.get("https://api.orbit.devotel.io/api/v1/voice/conferences",
                     headers=headers, params={"status": "in-progress", "limit": 20})
    print(r.json())
    ```

    ```go Go theme={null}
    package main

    import (
    	"net/http"
    	"net/url"
    	"os"
    )

    func main() {
    	u, _ := url.Parse("https://api.orbit.devotel.io/api/v1/voice/conferences")
    	q := u.Query()
    	q.Set("status", "in-progress")
    	q.Set("limit", "20")
    	u.RawQuery = q.Encode()
    	req, _ := http.NewRequest("GET", u.String(), nil)
    	req.Header.Set("X-API-Key", os.Getenv("ORBIT_API_KEY"))
    	http.DefaultClient.Do(req)
    }
    ```

    ```php PHP theme={null}
    <?php
    $ch = curl_init('https://api.orbit.devotel.io/api/v1/voice/conferences?status=in-progress&limit=20');
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($ch, CURLOPT_HTTPHEADER, ['X-API-Key: ' . getenv('ORBIT_API_KEY')]);
    echo curl_exec($ch);
    ```
  </CodeGroup>
</RequestExample>

<ResponseExample>
  ```json 200 theme={null}
  {
    "data": [
      {
        "id": "conf_abc1234567890abcdef1234567890abcd",
        "name": "Q1 Planning Call",
        "status": "in-progress",
        "caller_id": "+18005551234",
        "started_at": "2026-03-08T12:00:00Z",
        "ended_at": null,
        "duration_seconds": null,
        "max_participants": 25,
        "recording_url": null,
        "created_by": "usr_xyz",
        "created_at": "2026-03-08T12:00:00Z"
      }
    ],
    "meta": {
      "request_id": "req_conf_list_001",
      "cursor": null,
      "has_more": false,
      "timestamp": "2026-03-08T12:00:01Z"
    }
  }
  ```
</ResponseExample>

### Get Conference

`GET /api/v1/voice/conferences/{id}`

Returns a single conference with its participants embedded under `participants`. For rooms with more than 32 legs, paginate via `GET /api/v1/voice/conferences/{id}/participants`.

<RequestExample>
  <CodeGroup>
    ```bash cURL theme={null}
    curl -X GET "https://api.orbit.devotel.io/api/v1/voice/conferences/conf_abc123" \
      -H "X-API-Key: dv_live_sk_your_key_here"
    ```

    ```typescript Node.js theme={null}
    const conf = await orbit.voice.conferences.get('conf_abc123')
    console.log(conf.data.name, conf.data.participants.length)
    ```

    ```python Python theme={null}
    import os, requests
    headers = {"X-API-Key": os.environ["ORBIT_API_KEY"]}
    r = requests.get("https://api.orbit.devotel.io/api/v1/voice/conferences/conf_abc123", headers=headers)
    print(r.json())
    ```

    ```go Go theme={null}
    package main

    import (
    	"net/http"
    	"os"
    )

    func main() {
    	req, _ := http.NewRequest("GET",
    		"https://api.orbit.devotel.io/api/v1/voice/conferences/conf_abc123", nil)
    	req.Header.Set("X-API-Key", os.Getenv("ORBIT_API_KEY"))
    	http.DefaultClient.Do(req)
    }
    ```

    ```php PHP theme={null}
    <?php
    $ch = curl_init('https://api.orbit.devotel.io/api/v1/voice/conferences/conf_abc123');
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($ch, CURLOPT_HTTPHEADER, ['X-API-Key: ' . getenv('ORBIT_API_KEY')]);
    echo curl_exec($ch);
    ```
  </CodeGroup>
</RequestExample>

<ResponseExample>
  ```json 200 theme={null}
  {
    "data": {
      "id": "conf_abc1234567890abcdef1234567890abcd",
      "name": "Q1 Planning Call",
      "status": "in-progress",
      "caller_id": "+18005551234",
      "started_at": "2026-03-08T12:00:00Z",
      "ended_at": null,
      "duration_seconds": null,
      "max_participants": 25,
      "recording_url": null,
      "participants": [
        {
          "id": "cpart_abc",
          "phone_number": "+14155552671",
          "status": "answered",
          "leg_call_sid": "CA-…",
          "joined_at": "2026-03-08T12:00:14Z",
          "left_at": null,
          "is_host": true
        }
      ]
    },
    "meta": {
      "request_id": "req_conf_get_001",
      "timestamp": "2026-03-08T12:00:15Z"
    }
  }
  ```
</ResponseExample>

### Get Aggregate Metrics

`GET /api/v1/voice/conferences/aggregate`

Returns server-side rollup KPIs over a rolling window (default 30 days). Powers the dashboard's conference stat strip — avg duration, success rate, peak concurrent participants. Pass `window_days` to widen or narrow the rollup (max 365).

<ParamField query="window_days" type="integer" default="30">
  Window in days the rollup spans (1-365).
</ParamField>

<RequestExample>
  <CodeGroup>
    ```bash cURL theme={null}
    curl -X GET "https://api.orbit.devotel.io/api/v1/voice/conferences/aggregate?window_days=7" \
      -H "X-API-Key: dv_live_sk_your_key_here"
    ```

    ```typescript Node.js theme={null}
    const metrics = await orbit.voice.conferences.aggregate({ windowDays: 7 })
    console.log(
      metrics.data.avg_duration_seconds,
      metrics.data.success_rate,
      metrics.data.peak_concurrent_participants,
    )
    ```

    ```python Python theme={null}
    import os, requests
    headers = {"X-API-Key": os.environ["ORBIT_API_KEY"]}
    r = requests.get("https://api.orbit.devotel.io/api/v1/voice/conferences/aggregate",
                     headers=headers, params={"window_days": 7})
    print(r.json())
    ```

    ```go Go theme={null}
    package main

    import (
    	"net/http"
    	"os"
    )

    func main() {
    	req, _ := http.NewRequest("GET",
    		"https://api.orbit.devotel.io/api/v1/voice/conferences/aggregate?window_days=7", nil)
    	req.Header.Set("X-API-Key", os.Getenv("ORBIT_API_KEY"))
    	http.DefaultClient.Do(req)
    }
    ```

    ```php PHP theme={null}
    <?php
    $ch = curl_init('https://api.orbit.devotel.io/api/v1/voice/conferences/aggregate?window_days=7');
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($ch, CURLOPT_HTTPHEADER, ['X-API-Key: ' . getenv('ORBIT_API_KEY')]);
    echo curl_exec($ch);
    ```
  </CodeGroup>
</RequestExample>

<ResponseExample>
  ```json 200 theme={null}
  {
    "data": {
      "total_count": 142,
      "active_count": 3,
      "avg_duration_seconds": 1820,
      "success_rate": 0.9437,
      "peak_concurrent_participants": 18,
      "total_participants_window": 612,
      "window_days": 7
    },
    "meta": {
      "request_id": "req_conf_agg_001",
      "timestamp": "2026-03-08T12:00:00Z"
    }
  }
  ```
</ResponseExample>

### Create Conference

`POST /api/v1/voice/conferences`

<ParamField body="name" type="string" required>
  Conference name (max 200 chars).
</ParamField>

<ParamField body="participants" type="string[]" required>
  Seed participants to dial when the conference is created. Each entry must be E.164. 0-32 entries (pass an empty array to create an empty conference).
</ParamField>

<ParamField body="from" type="string">
  E.164 caller-id used for each seed-participant dial. Must be a voice-capable number owned by your organization.
</ParamField>

<ParamField body="record" type="boolean" default="false">
  Record every participant leg on creation. Subject to per-jurisdiction recording consent gates.
</ParamField>

<ParamField body="maxParticipants" type="integer" default="8">
  Cap on simultaneously-bridged legs (max 32).
</ParamField>

<RequestExample>
  <CodeGroup>
    ```bash cURL theme={null}
    curl -X POST "https://api.orbit.devotel.io/api/v1/voice/conferences" \
      -H "X-API-Key: dv_live_sk_your_key_here" \
      -H "Content-Type: application/json" \
      -d '{
      "name": "Q1 Planning Call",
      "participants": ["+14155552671", "+14155552672"],
      "from": "+18005551234",
      "record": true,
      "maxParticipants": 25
    }'
    ```

    ```typescript Node.js theme={null}
    const conf = await orbit.voice.conferences.create({
      name: 'Q1 Planning Call',
      participants: ['+14155552671', '+14155552672'],
      from: '+18005551234',
      record: true,
      maxParticipants: 25,
    })
    console.log(conf.data.id)
    ```

    ```python Python theme={null}
    import os, requests
    headers = {"X-API-Key": os.environ["ORBIT_API_KEY"], "Content-Type": "application/json"}
    r = requests.post("https://api.orbit.devotel.io/api/v1/voice/conferences", headers=headers, json={
      "name": "Q1 Planning Call",
      "participants": ["+14155552671", "+14155552672"],
      "from": "+18005551234",
      "record": True,
      "maxParticipants": 25
    })
    print(r.json())
    ```

    ```go Go theme={null}
    package main

    import (
    	"bytes"
    	"net/http"
    	"os"
    )

    func main() {
    	req, _ := http.NewRequest("POST", "https://api.orbit.devotel.io/api/v1/voice/conferences", bytes.NewBuffer([]byte(`{
      "name": "Q1 Planning Call",
      "participants": ["+14155552671", "+14155552672"],
      "from": "+18005551234",
      "record": true,
      "maxParticipants": 25
    }`)))
    	req.Header.Set("X-API-Key", os.Getenv("ORBIT_API_KEY"))
    	req.Header.Set("Content-Type", "application/json")
    	http.DefaultClient.Do(req)
    }
    ```

    ```php PHP theme={null}
    <?php
    $ch = curl_init('https://api.orbit.devotel.io/api/v1/voice/conferences');
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST');
    curl_setopt($ch, CURLOPT_HTTPHEADER, [
      'X-API-Key: ' . getenv('ORBIT_API_KEY'),
      'Content-Type: application/json',
    ]);
    curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode([
      'name' => 'Q1 Planning Call',
      'participants' => ['+14155552671', '+14155552672'],
      'from' => '+18005551234',
      'record' => true,
      'maxParticipants' => 25,
    ]));
    echo curl_exec($ch);
    ```
  </CodeGroup>
</RequestExample>

<ResponseExample>
  ```json 201 theme={null}
  {
    "data": {
      "id": "conf_abc123",
      "name": "Q1 Planning Call",
      "status": "waiting",
      "pin": "482901",
      "dial_in_number": "+18005559999",
      "max_participants": 25,
      "record": true,
      "created_at": "2026-03-08T12:00:00Z"
    },
    "meta": {
      "request_id": "req_conf_001",
      "timestamp": "2026-03-08T12:00:00Z"
    }
  }
  ```
</ResponseExample>

### Add Participant

`POST /api/v1/voice/conferences/{id}/participants`

<ParamField body="phone_number" type="string" required>
  E.164 phone number to dial into the conference (e.g. `+14155552671`)
</ParamField>

<ParamField body="from" type="string">
  Optional caller ID to present on the dialed leg. Defaults to the conference's configured outbound number when omitted.
</ParamField>

<RequestExample>
  <CodeGroup>
    ```bash cURL theme={null}
    curl -X POST "https://api.orbit.devotel.io/api/v1/voice/conferences/conf_abc123/participants" \
      -H "X-API-Key: dv_live_sk_your_key_here" \
      -H "Content-Type: application/json" \
      -d '{ "phone_number": "+14155552671" }'
    ```

    ```typescript Node.js theme={null}
    await orbit.voice.conferences.addParticipant('conf_abc123', {
      phone_number: '+14155552671',
    })
    ```

    ```python Python theme={null}
    import os, requests
    headers = {"X-API-Key": os.environ["ORBIT_API_KEY"], "Content-Type": "application/json"}
    r = requests.post("https://api.orbit.devotel.io/api/v1/voice/conferences/conf_abc123/participants",
                      headers=headers, json={"phone_number": "+14155552671"})
    print(r.json())
    ```

    ```go Go theme={null}
    package main

    import (
    	"bytes"
    	"net/http"
    	"os"
    )

    func main() {
    	req, _ := http.NewRequest("POST",
    		"https://api.orbit.devotel.io/api/v1/voice/conferences/conf_abc123/participants",
    		bytes.NewBuffer([]byte(`{"phone_number":"+14155552671"}`)))
    	req.Header.Set("X-API-Key", os.Getenv("ORBIT_API_KEY"))
    	req.Header.Set("Content-Type", "application/json")
    	http.DefaultClient.Do(req)
    }
    ```

    ```php PHP theme={null}
    <?php
    $ch = curl_init('https://api.orbit.devotel.io/api/v1/voice/conferences/conf_abc123/participants');
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST');
    curl_setopt($ch, CURLOPT_HTTPHEADER, [
      'X-API-Key: ' . getenv('ORBIT_API_KEY'),
      'Content-Type: application/json',
    ]);
    curl_setopt($ch, CURLOPT_POSTFIELDS, '{"phone_number":"+14155552671"}');
    echo curl_exec($ch);
    ```
  </CodeGroup>
</RequestExample>

### Remove Participant

`DELETE /api/v1/voice/conferences/{id}/participants/{participantId}`

<RequestExample>
  <CodeGroup>
    ```bash cURL theme={null}
    curl -X DELETE "https://api.orbit.devotel.io/api/v1/voice/conferences/conf_abc123/participants/part_xyz" \
      -H "X-API-Key: dv_live_sk_your_key_here"
    ```

    ```typescript Node.js theme={null}
    await orbit.voice.conferences.removeParticipant('conf_abc123', 'part_xyz')
    ```

    ```python Python theme={null}
    import os, requests
    headers = {"X-API-Key": os.environ["ORBIT_API_KEY"]}
    r = requests.delete("https://api.orbit.devotel.io/api/v1/voice/conferences/conf_abc123/participants/part_xyz",
                        headers=headers)
    print(r.status_code)
    ```

    ```go Go theme={null}
    package main

    import (
    	"net/http"
    	"os"
    )

    func main() {
    	req, _ := http.NewRequest("DELETE",
    		"https://api.orbit.devotel.io/api/v1/voice/conferences/conf_abc123/participants/part_xyz", nil)
    	req.Header.Set("X-API-Key", os.Getenv("ORBIT_API_KEY"))
    	http.DefaultClient.Do(req)
    }
    ```

    ```php PHP theme={null}
    <?php
    $ch = curl_init('https://api.orbit.devotel.io/api/v1/voice/conferences/conf_abc123/participants/part_xyz');
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'DELETE');
    curl_setopt($ch, CURLOPT_HTTPHEADER, ['X-API-Key: ' . getenv('ORBIT_API_KEY')]);
    echo curl_exec($ch);
    ```
  </CodeGroup>
</RequestExample>

### End Conference

`POST /api/v1/voice/conferences/{id}/end`

<RequestExample>
  <CodeGroup>
    ```bash cURL theme={null}
    curl -X POST "https://api.orbit.devotel.io/api/v1/voice/conferences/conf_abc123/end" \
      -H "X-API-Key: dv_live_sk_your_key_here"
    ```

    ```typescript Node.js theme={null}
    await orbit.voice.conferences.end('conf_abc123')
    ```

    ```python Python theme={null}
    import os, requests
    headers = {"X-API-Key": os.environ["ORBIT_API_KEY"]}
    r = requests.post("https://api.orbit.devotel.io/api/v1/voice/conferences/conf_abc123/end", headers=headers)
    print(r.json())
    ```

    ```go Go theme={null}
    package main

    import (
    	"net/http"
    	"os"
    )

    func main() {
    	req, _ := http.NewRequest("POST",
    		"https://api.orbit.devotel.io/api/v1/voice/conferences/conf_abc123/end", nil)
    	req.Header.Set("X-API-Key", os.Getenv("ORBIT_API_KEY"))
    	http.DefaultClient.Do(req)
    }
    ```

    ```php PHP theme={null}
    <?php
    $ch = curl_init('https://api.orbit.devotel.io/api/v1/voice/conferences/conf_abc123/end');
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST');
    curl_setopt($ch, CURLOPT_HTTPHEADER, ['X-API-Key: ' . getenv('ORBIT_API_KEY')]);
    echo curl_exec($ch);
    ```
  </CodeGroup>
</RequestExample>

***

## Call Forwarding (Find-me / Follow-me)

Per-user call-forwarding rules — where a user's inbound calls ring. Each user has at most one rule covering the handling `mode`, an ordered list of `destinations`, a per-leg ring timeout, an optional active window, and an anonymous-call rejection toggle. Any authenticated user manages **their own** rule; org owners/admins can also manage rules on behalf of other users in the organization for onboarding and offboarding.

`mode` is one of:

| Mode                    | Behaviour                                                                         |
| ----------------------- | --------------------------------------------------------------------------------- |
| `none`                  | Ring the user's own registered devices only (no forwarding).                      |
| `forward_all`           | Forward every inbound call to the `destinations`.                                 |
| `sim_ring`              | Ring the user's devices and the `destinations` simultaneously.                    |
| `sequential`            | Ring the `destinations` one after another (find-me / follow-me).                  |
| `forward_after_timeout` | Ring the user's devices, then forward to `destinations` after `ring_timeout_sec`. |

<Note>
  PSTN forwarding destinations are configuration only. The outbound leg that dials a forwarded `pstn` number is placed over the Devotel wholesale softswitch — the same termination path as every other outbound call.
</Note>

### Get My Rule

`GET /api/v1/voice/call-forwarding`

Returns the authenticated user's own rule. When the user has never configured forwarding, a synthetic `none` rule is returned (this endpoint never 404s).

<RequestExample>
  <CodeGroup>
    ```bash cURL theme={null}
    curl "https://api.orbit.devotel.io/api/v1/voice/call-forwarding" \
      -H "X-API-Key: dv_live_sk_your_key_here"
    ```

    ```typescript Node.js theme={null}
    const rule = await orbit.voice.callForwarding.get()
    ```

    ```python Python theme={null}
    import os, requests
    headers = {"X-API-Key": os.environ["ORBIT_API_KEY"]}
    r = requests.get("https://api.orbit.devotel.io/api/v1/voice/call-forwarding", headers=headers)
    print(r.json())
    ```
  </CodeGroup>
</RequestExample>

<ResponseExample>
  ```json 200 theme={null}
  {
    "data": {
      "mode": "sequential",
      "destinations": [
        { "kind": "pstn", "value": "+15551234567", "label": "Mobile" },
        { "kind": "sip", "value": "sip:alex@example.com" }
      ],
      "ring_timeout_sec": 20,
      "active_from": null,
      "active_until": null,
      "note": "Out of office until Monday",
      "reject_anonymous": false
    },
    "meta": {
      "request_id": "req_cf_001",
      "timestamp": "2026-06-28T12:00:00Z"
    }
  }
  ```
</ResponseExample>

### Upsert My Rule

`PUT /api/v1/voice/call-forwarding`

Creates or replaces the authenticated user's rule (idempotent upsert keyed on the user).

<ParamField body="mode" type="string" required>
  One of `none`, `forward_all`, `sim_ring`, `sequential`, `forward_after_timeout`.
</ParamField>

<ParamField body="destinations" type="object[]">
  Ordered forwarding targets (max 5). Each entry is `{ kind, value, label? }`. `kind` is `pstn` (the `value` must be an E.164 number, for example `+15551234567`) or `sip` (the `value` must be a `sip:`-prefixed URI). A malformed destination returns 400. Defaults to an empty array.
</ParamField>

<ParamField body="ring_timeout_sec" type="integer">
  Seconds to ring before the rule falls through to the next leg or voicemail (5–120). Defaults to 20.
</ParamField>

<ParamField body="active_from" type="string | null">
  ISO-8601 timestamp the rule starts applying. `null` or omitted means always-on.
</ParamField>

<ParamField body="active_until" type="string | null">
  ISO-8601 timestamp the rule stops applying. `null` or omitted means always-on.
</ParamField>

<ParamField body="note" type="string | null">
  Optional free-text note (max 280 characters).
</ParamField>

<ParamField body="reject_anonymous" type="boolean">
  When `true`, inbound legs with a withheld/anonymous caller-id are declined before any fan-out. Defaults to `false`.
</ParamField>

<RequestExample>
  <CodeGroup>
    ```bash cURL theme={null}
    curl -X PUT "https://api.orbit.devotel.io/api/v1/voice/call-forwarding" \
      -H "X-API-Key: dv_live_sk_your_key_here" \
      -H "Content-Type: application/json" \
      -d '{
      "mode": "sequential",
      "destinations": [
        { "kind": "pstn", "value": "+15551234567", "label": "Mobile" },
        { "kind": "sip", "value": "sip:alex@example.com" }
      ],
      "ring_timeout_sec": 20
    }'
    ```

    ```typescript Node.js theme={null}
    const rule = await orbit.voice.callForwarding.set({
      mode: 'sequential',
      destinations: [
        { kind: 'pstn', value: '+15551234567', label: 'Mobile' },
        { kind: 'sip', value: 'sip:alex@example.com' },
      ],
      ring_timeout_sec: 20,
    })
    ```

    ```python Python theme={null}
    import os, requests
    headers = {"X-API-Key": os.environ["ORBIT_API_KEY"], "Content-Type": "application/json"}
    r = requests.put("https://api.orbit.devotel.io/api/v1/voice/call-forwarding", headers=headers, json={
      "mode": "sequential",
      "destinations": [
        {"kind": "pstn", "value": "+15551234567", "label": "Mobile"},
        {"kind": "sip", "value": "sip:alex@example.com"}
      ],
      "ring_timeout_sec": 20
    })
    print(r.json())
    ```
  </CodeGroup>
</RequestExample>

<ResponseExample>
  ```json 200 theme={null}
  {
    "data": {
      "mode": "sequential",
      "destinations": [
        { "kind": "pstn", "value": "+15551234567", "label": "Mobile" },
        { "kind": "sip", "value": "sip:alex@example.com" }
      ],
      "ring_timeout_sec": 20,
      "active_from": null,
      "active_until": null,
      "note": null,
      "reject_anonymous": false
    },
    "meta": {
      "request_id": "req_cf_002",
      "timestamp": "2026-06-28T12:00:00Z"
    }
  }
  ```
</ResponseExample>

### Clear My Rule

`DELETE /api/v1/voice/call-forwarding`

Reverts the authenticated user's rule back to `none` in one shot — clears destinations, active window, note, and anonymous-call rejection. Returns `204 No Content`.

### List Org Rules (admin)

`GET /api/v1/voice/call-forwarding/users`

Org owner/admin only. Returns every configured per-user rule in the organization, each annotated with the user's name, email, and last-updated timestamp.

`GET /api/v1/voice/call-forwarding/users/{userId}`

`PUT /api/v1/voice/call-forwarding/users/{userId}`

`DELETE /api/v1/voice/call-forwarding/users/{userId}`

Org owner/admin only. Read, upsert (same body as the self-service `PUT` above), or clear the rule for another user in your organization. The target user must belong to your organization, otherwise the request returns 404.

<ResponseExample>
  ```json 200 theme={null}
  {
    "data": {
      "users": [
        {
          "user_id": "usr_abc123",
          "user_name": "Alex Rivera",
          "user_email": "alex@example.com",
          "updated_at": "2026-06-27T09:30:00Z",
          "mode": "forward_after_timeout",
          "destinations": [{ "kind": "pstn", "value": "+15551234567" }],
          "ring_timeout_sec": 25,
          "active_from": null,
          "active_until": null,
          "note": null,
          "reject_anonymous": true
        }
      ]
    },
    "meta": {
      "request_id": "req_cf_003",
      "timestamp": "2026-06-28T12:00:00Z"
    }
  }
  ```
</ResponseExample>

***

## Voicemail

Manage voicemail boxes for receiving and transcribing voice messages when calls go unanswered.

### Create Voicemail Box

`POST /api/v1/voice/voicemail-boxes`

Creates a department voicemail box scoped to your organization. A box is a shared mailbox (for example `sales` or `support`): voicemails captured against a box-bound number are visible to every member you list, and to no one else.

<ParamField body="name" type="string" required>
  Slug-shaped identifier, unique within your organization. Lowercase `a-z`, digits, hyphen or underscore; the first character must be a letter or digit. Max 64 characters (for example `sales`, `support-eu`).
</ParamField>

<ParamField body="label" type="string" required>
  Human-readable display label shown in the dashboard (1–120 characters).
</ParamField>

<ParamField body="members" type="string[]">
  User ids subscribed to this box (max 50). Every id must belong to your organization, otherwise the request returns 422. Omit to create an empty roster you can populate later with `PUT`.
</ParamField>

<ParamField body="retention_days" type="integer | null">
  Voicemail retention window in days (1–2555). Set to `null` or omit to inherit your organization's default retention.
</ParamField>

<RequestExample>
  <CodeGroup>
    ```bash cURL theme={null}
    curl -X POST "https://api.orbit.devotel.io/api/v1/voice/voicemail-boxes" \
      -H "X-API-Key: dv_live_sk_your_key_here" \
      -H "Content-Type: application/json" \
      -d '{
      "name": "support",
      "label": "Support Team",
      "members": ["usr_abc123", "usr_def456"],
      "retention_days": 90
    }'
    ```

    ```typescript Node.js theme={null}
    const box = await orbit.voice.voicemailBoxes.create({
      name: 'support',
      label: 'Support Team',
      members: ['usr_abc123', 'usr_def456'],
      retention_days: 90,
    })
    ```

    ```python Python theme={null}
    import os, requests
    headers = {"X-API-Key": os.environ["ORBIT_API_KEY"], "Content-Type": "application/json"}
    r = requests.post("https://api.orbit.devotel.io/api/v1/voice/voicemail-boxes", headers=headers, json={
      "name": "support",
      "label": "Support Team",
      "members": ["usr_abc123", "usr_def456"],
      "retention_days": 90
    })
    print(r.json())
    ```

    ```go Go theme={null}
    package main

    import (
    	"bytes"
    	"net/http"
    	"os"
    )

    func main() {
    	req, _ := http.NewRequest("POST", "https://api.orbit.devotel.io/api/v1/voice/voicemail-boxes", bytes.NewBuffer([]byte(`{
      "name": "support",
      "label": "Support Team",
      "members": ["usr_abc123", "usr_def456"],
      "retention_days": 90
    }`)))
    	req.Header.Set("X-API-Key", os.Getenv("ORBIT_API_KEY"))
    	req.Header.Set("Content-Type", "application/json")
    	http.DefaultClient.Do(req)
    }
    ```

    ```php PHP theme={null}
    <?php
    $ch = curl_init('https://api.orbit.devotel.io/api/v1/voice/voicemail-boxes');
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST');
    curl_setopt($ch, CURLOPT_HTTPHEADER, [
      'X-API-Key: ' . getenv('ORBIT_API_KEY'),
      'Content-Type: application/json',
    ]);
    curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode([
      'name' => 'support',
      'label' => 'Support Team',
      'members' => ['usr_abc123', 'usr_def456'],
      'retention_days' => 90,
    ]));
    echo curl_exec($ch);
    ```
  </CodeGroup>
</RequestExample>

<ResponseExample>
  ```json 201 theme={null}
  {
    "data": {
      "id": "vmbox_abc123",
      "organization_id": "org_acme",
      "name": "support",
      "label": "Support Team",
      "members": ["usr_abc123", "usr_def456"],
      "inbound_route_id": null,
      "retention_days": 90,
      "created_at": "2026-03-08T12:00:00Z",
      "updated_at": "2026-03-08T12:00:00Z"
    },
    "meta": {
      "request_id": "req_vmail_001",
      "timestamp": "2026-03-08T12:00:00Z"
    }
  }
  ```
</ResponseExample>

### List Voicemail Messages

`GET /api/v1/voice/voicemails`

Retrieve voicemail messages across the org with transcriptions, with cursor pagination and filters by direction, status, and unread state.

`GET /api/v1/voice/voicemails/{id}`

Get a single voicemail message including transcription and recording URL.

`PATCH /api/v1/voice/voicemails/{id}/read`

Mark a voicemail as read.

`DELETE /api/v1/voice/voicemails/{id}`

Permanently delete a voicemail message.

<RequestExample>
  <CodeGroup>
    ```bash cURL theme={null}
    # List
    curl -X GET "https://api.orbit.devotel.io/api/v1/voice/voicemails?status=unread&limit=20" \
      -H "X-API-Key: dv_live_sk_your_key_here"
    # Get
    curl -X GET "https://api.orbit.devotel.io/api/v1/voice/voicemails/vmsg_001" \
      -H "X-API-Key: dv_live_sk_your_key_here"
    # Mark read
    curl -X PATCH "https://api.orbit.devotel.io/api/v1/voice/voicemails/vmsg_001/read" \
      -H "X-API-Key: dv_live_sk_your_key_here"
    # Delete
    curl -X DELETE "https://api.orbit.devotel.io/api/v1/voice/voicemails/vmsg_001" \
      -H "X-API-Key: dv_live_sk_your_key_here"
    ```

    ```typescript Node.js theme={null}
    await orbit.voice.voicemails.list({ status: 'unread', limit: 20 })
    await orbit.voice.voicemails.get('vmsg_001')
    await orbit.voice.voicemails.markRead('vmsg_001')
    await orbit.voice.voicemails.delete('vmsg_001')
    ```

    ```python Python theme={null}
    import os, requests
    h = {"X-API-Key": os.environ["ORBIT_API_KEY"]}
    requests.get("https://api.orbit.devotel.io/api/v1/voice/voicemails",
                 headers=h, params={"status": "unread", "limit": 20})
    requests.get("https://api.orbit.devotel.io/api/v1/voice/voicemails/vmsg_001", headers=h)
    requests.patch("https://api.orbit.devotel.io/api/v1/voice/voicemails/vmsg_001/read", headers=h)
    requests.delete("https://api.orbit.devotel.io/api/v1/voice/voicemails/vmsg_001", headers=h)
    ```

    ```go Go theme={null}
    package main

    import (
    	"net/http"
    	"os"
    )

    func main() {
    	req, _ := http.NewRequest("GET", "https://api.orbit.devotel.io/api/v1/voice/voicemails?status=unread&limit=20", nil)
    	req.Header.Set("X-API-Key", os.Getenv("ORBIT_API_KEY"))
    	http.DefaultClient.Do(req)
    }
    ```

    ```php PHP theme={null}
    <?php
    $ch = curl_init('https://api.orbit.devotel.io/api/v1/voice/voicemails?status=unread&limit=20');
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($ch, CURLOPT_HTTPHEADER, ['X-API-Key: ' . getenv('ORBIT_API_KEY')]);
    echo curl_exec($ch);
    ```
  </CodeGroup>
</RequestExample>

<ResponseExample>
  ```json 200 theme={null}
  {
    "data": [
      {
        "id": "vmsg_001",
        "voicemail_id": "vmail_abc123",
        "caller": "+14155552671",
        "duration_seconds": 35,
        "transcription": "Hi, this is Jane. I wanted to follow up on my order. Please call me back.",
        "recording_url": "https://storage.devotel.io/voicemail/vmsg_001.wav",
        "listened": false,
        "created_at": "2026-03-08T14:30:00Z"
      }
    ],
    "meta": {
      "request_id": "req_vmsg_001",
      "timestamp": "2026-03-08T15:00:00Z",
      "pagination": {
        "cursor": "cur_vmsg_abc",
        "has_more": false,
        "total": 1
      }
    }
  }
  ```
</ResponseExample>

***

## Conference Operations (advanced)

Beyond `Add Participant` / `Remove Participant`, the conference API exposes a complete moderation surface.

### Mute / Unmute a Participant

`POST /api/v1/voice/conferences/{id}/participants/{participantId}/mute`

`POST /api/v1/voice/conferences/{id}/participants/{participantId}/unmute`

<RequestExample>
  <CodeGroup>
    ```bash cURL theme={null}
    curl -X POST "https://api.orbit.devotel.io/api/v1/voice/conferences/conf_abc123/participants/part_xyz/mute" \
      -H "X-API-Key: dv_live_sk_your_key_here"
    ```

    ```typescript Node.js theme={null}
    await orbit.voice.conferences.muteParticipant('conf_abc123', 'part_xyz')
    await orbit.voice.conferences.unmuteParticipant('conf_abc123', 'part_xyz')
    ```

    ```python Python theme={null}
    import os, requests
    headers = {"X-API-Key": os.environ["ORBIT_API_KEY"]}
    r = requests.post(
      "https://api.orbit.devotel.io/api/v1/voice/conferences/conf_abc123/participants/part_xyz/mute",
      headers=headers)
    print(r.json())
    ```

    ```go Go theme={null}
    package main

    import (
    	"net/http"
    	"os"
    )

    func main() {
    	req, _ := http.NewRequest("POST",
    		"https://api.orbit.devotel.io/api/v1/voice/conferences/conf_abc123/participants/part_xyz/mute", nil)
    	req.Header.Set("X-API-Key", os.Getenv("ORBIT_API_KEY"))
    	http.DefaultClient.Do(req)
    }
    ```

    ```php PHP theme={null}
    <?php
    $ch = curl_init('https://api.orbit.devotel.io/api/v1/voice/conferences/conf_abc123/participants/part_xyz/mute');
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST');
    curl_setopt($ch, CURLOPT_HTTPHEADER, ['X-API-Key: ' . getenv('ORBIT_API_KEY')]);
    echo curl_exec($ch);
    ```
  </CodeGroup>
</RequestExample>

### Mute / Unmute Everyone

`POST /api/v1/voice/conferences/{id}/mute-all`

`POST /api/v1/voice/conferences/{id}/unmute-all`

Useful for "all-hands" conferences where the moderator wants to control the floor. Already-muted participants keep their state.

<RequestExample>
  <CodeGroup>
    ```bash cURL theme={null}
    curl -X POST "https://api.orbit.devotel.io/api/v1/voice/conferences/conf_abc123/mute-all" \
      -H "X-API-Key: dv_live_sk_your_key_here"
    ```

    ```typescript Node.js theme={null}
    await orbit.voice.conferences.muteAll('conf_abc123')
    await orbit.voice.conferences.unmuteAll('conf_abc123')
    ```

    ```python Python theme={null}
    import os, requests
    headers = {"X-API-Key": os.environ["ORBIT_API_KEY"]}
    r = requests.post("https://api.orbit.devotel.io/api/v1/voice/conferences/conf_abc123/mute-all", headers=headers)
    print(r.json())
    ```

    ```go Go theme={null}
    package main

    import (
    	"net/http"
    	"os"
    )

    func main() {
    	req, _ := http.NewRequest("POST",
    		"https://api.orbit.devotel.io/api/v1/voice/conferences/conf_abc123/mute-all", nil)
    	req.Header.Set("X-API-Key", os.Getenv("ORBIT_API_KEY"))
    	http.DefaultClient.Do(req)
    }
    ```

    ```php PHP theme={null}
    <?php
    $ch = curl_init('https://api.orbit.devotel.io/api/v1/voice/conferences/conf_abc123/mute-all');
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST');
    curl_setopt($ch, CURLOPT_HTTPHEADER, ['X-API-Key: ' . getenv('ORBIT_API_KEY')]);
    echo curl_exec($ch);
    ```
  </CodeGroup>
</RequestExample>

### Hold / Unhold a Participant

`POST /api/v1/voice/conferences/{id}/participants/{participantId}/hold`

`POST /api/v1/voice/conferences/{id}/participants/{participantId}/unhold`

<RequestExample>
  <CodeGroup>
    ```bash cURL theme={null}
    curl -X POST "https://api.orbit.devotel.io/api/v1/voice/conferences/conf_abc123/participants/part_xyz/hold" \
      -H "X-API-Key: dv_live_sk_your_key_here"
    ```

    ```typescript Node.js theme={null}
    await orbit.voice.conferences.holdParticipant('conf_abc123', 'part_xyz')
    await orbit.voice.conferences.unholdParticipant('conf_abc123', 'part_xyz')
    ```

    ```python Python theme={null}
    import os, requests
    headers = {"X-API-Key": os.environ["ORBIT_API_KEY"]}
    r = requests.post(
      "https://api.orbit.devotel.io/api/v1/voice/conferences/conf_abc123/participants/part_xyz/hold",
      headers=headers)
    print(r.json())
    ```

    ```go Go theme={null}
    package main

    import (
    	"net/http"
    	"os"
    )

    func main() {
    	req, _ := http.NewRequest("POST",
    		"https://api.orbit.devotel.io/api/v1/voice/conferences/conf_abc123/participants/part_xyz/hold", nil)
    	req.Header.Set("X-API-Key", os.Getenv("ORBIT_API_KEY"))
    	http.DefaultClient.Do(req)
    }
    ```

    ```php PHP theme={null}
    <?php
    $ch = curl_init('https://api.orbit.devotel.io/api/v1/voice/conferences/conf_abc123/participants/part_xyz/hold');
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST');
    curl_setopt($ch, CURLOPT_HTTPHEADER, ['X-API-Key: ' . getenv('ORBIT_API_KEY')]);
    echo curl_exec($ch);
    ```
  </CodeGroup>
</RequestExample>

### Lock / Unlock Conference

`POST /api/v1/voice/conferences/{id}/lock`

`POST /api/v1/voice/conferences/{id}/unlock`

When locked, no new participants can join — existing participants stay connected. Use to seal a conference for a confidential discussion.

<RequestExample>
  <CodeGroup>
    ```bash cURL theme={null}
    curl -X POST "https://api.orbit.devotel.io/api/v1/voice/conferences/conf_abc123/lock" \
      -H "X-API-Key: dv_live_sk_your_key_here"
    ```

    ```typescript Node.js theme={null}
    await orbit.voice.conferences.lock('conf_abc123')
    await orbit.voice.conferences.unlock('conf_abc123')
    ```

    ```python Python theme={null}
    import os, requests
    headers = {"X-API-Key": os.environ["ORBIT_API_KEY"]}
    r = requests.post("https://api.orbit.devotel.io/api/v1/voice/conferences/conf_abc123/lock", headers=headers)
    print(r.json())
    ```

    ```go Go theme={null}
    package main

    import (
    	"net/http"
    	"os"
    )

    func main() {
    	req, _ := http.NewRequest("POST",
    		"https://api.orbit.devotel.io/api/v1/voice/conferences/conf_abc123/lock", nil)
    	req.Header.Set("X-API-Key", os.Getenv("ORBIT_API_KEY"))
    	http.DefaultClient.Do(req)
    }
    ```

    ```php PHP theme={null}
    <?php
    $ch = curl_init('https://api.orbit.devotel.io/api/v1/voice/conferences/conf_abc123/lock');
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST');
    curl_setopt($ch, CURLOPT_HTTPHEADER, ['X-API-Key: ' . getenv('ORBIT_API_KEY')]);
    echo curl_exec($ch);
    ```
  </CodeGroup>
</RequestExample>

### End / Delete a Conference

`POST /api/v1/voice/conferences/{id}/end`

End the active conference and disconnect every participant gracefully. The conference row is retained for analytics.

`DELETE /api/v1/voice/conferences/{id}`

Hard-delete the conference row (only allowed when status is `ended`).

<RequestExample>
  <CodeGroup>
    ```bash cURL theme={null}
    curl -X DELETE "https://api.orbit.devotel.io/api/v1/voice/conferences/conf_abc123" \
      -H "X-API-Key: dv_live_sk_your_key_here"
    ```

    ```typescript Node.js theme={null}
    await orbit.voice.conferences.end('conf_abc123')
    await orbit.voice.conferences.delete('conf_abc123')
    ```

    ```python Python theme={null}
    import os, requests
    headers = {"X-API-Key": os.environ["ORBIT_API_KEY"]}
    requests.delete("https://api.orbit.devotel.io/api/v1/voice/conferences/conf_abc123", headers=headers)
    ```

    ```go Go theme={null}
    package main

    import (
    	"net/http"
    	"os"
    )

    func main() {
    	req, _ := http.NewRequest("DELETE",
    		"https://api.orbit.devotel.io/api/v1/voice/conferences/conf_abc123", nil)
    	req.Header.Set("X-API-Key", os.Getenv("ORBIT_API_KEY"))
    	http.DefaultClient.Do(req)
    }
    ```

    ```php PHP theme={null}
    <?php
    $ch = curl_init('https://api.orbit.devotel.io/api/v1/voice/conferences/conf_abc123');
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'DELETE');
    curl_setopt($ch, CURLOPT_HTTPHEADER, ['X-API-Key: ' . getenv('ORBIT_API_KEY')]);
    echo curl_exec($ch);
    ```
  </CodeGroup>
</RequestExample>

***

## Call Queues

Queue inbound calls by skill / priority and route them to the next available agent. Pairs with the AI agent layer for hybrid human + AI front-desk experiences.

### Create a Queue

`POST /api/v1/voice/queues`

<ParamField body="name" type="string" required>
  Queue name (visible to supervisors in the wallboard).
</ParamField>

<ParamField body="routingStrategy" type="string" default="longest-idle">
  Strategy used to pick an agent when several match a queued call. One of:
  `longest-idle` (oldest idle agent wins — default), `least-busy` (lowest
  occupancy), `skill-weighted` (highest weighted-proficiency score),
  `round-robin` (true rotation by last-dispatched), `fixed-order` (linear hunt
  by agent dispatch order), `percent-allocation` (weighted random by per-agent
  allocation percentage), `last-agent-preferred` (route back to the agent who
  last handled this caller, falling back to `longest-idle`).
</ParamField>

<ParamField body="maxWaitSeconds" type="integer" default="300">
  Maximum hold time (seconds, 30–3600) before the call falls through to
  `overflowAction`.
</ParamField>

<ParamField body="maxHoldSeconds" type="integer">
  Per-queue maximum hold-time ceiling (seconds, 60–3600). When a caller waits
  on hold longer than this, the queue raises a supervisor hold-time alert and
  starts the overflow-escalation path. Omitted leaves the ceiling unset (no
  hold-time escalation — the default).
</ParamField>

<ParamField body="overflowAction" type="string" default="voicemail">
  Action when max-wait expires. One of: `voicemail`, `callback`, `hangup`,
  `overflow_queue`. When set to `overflow_queue`, `overflowQueueId` is required.
</ParamField>

<ParamField body="overflowQueueId" type="string">
  Target queue id (same tenant) calls overflow into. Required when
  `overflowAction` is `overflow_queue`; must be omitted otherwise.
</ParamField>

<ParamField body="holdMusicUrl" type="string">
  Public URL of an MP3 played while waiting.
</ParamField>

<RequestExample>
  <CodeGroup>
    ```bash cURL theme={null}
    curl -X POST "https://api.orbit.devotel.io/api/v1/voice/queues" \
      -H "X-API-Key: dv_live_sk_your_key_here" \
      -H "Content-Type: application/json" \
      -d '{
      "name": "Support Tier 1",
      "routingStrategy": "longest-idle",
      "maxWaitSeconds": 240,
      "overflowAction": "voicemail",
      "holdMusicUrl": "https://cdn.example.com/hold-music.mp3"
    }'
    ```

    ```typescript Node.js theme={null}
    const q = await orbit.voice.queues.create({
      name: 'Support Tier 1',
      routingStrategy: 'longest-idle',
      maxWaitSeconds: 240,
      overflowAction: 'voicemail',
      holdMusicUrl: 'https://cdn.example.com/hold-music.mp3',
    })
    ```

    ```python Python theme={null}
    import os, requests
    headers = {"X-API-Key": os.environ["ORBIT_API_KEY"], "Content-Type": "application/json"}
    r = requests.post("https://api.orbit.devotel.io/api/v1/voice/queues", headers=headers, json={
      "name": "Support Tier 1",
      "routingStrategy": "longest-idle",
      "maxWaitSeconds": 240,
      "overflowAction": "voicemail",
      "holdMusicUrl": "https://cdn.example.com/hold-music.mp3"
    })
    print(r.json())
    ```

    ```go Go theme={null}
    package main

    import (
    	"bytes"
    	"net/http"
    	"os"
    )

    func main() {
    	req, _ := http.NewRequest("POST", "https://api.orbit.devotel.io/api/v1/voice/queues", bytes.NewBuffer([]byte(`{
      "name": "Support Tier 1",
      "routingStrategy": "longest-idle",
      "maxWaitSeconds": 240,
      "overflowAction": "voicemail"
    }`)))
    	req.Header.Set("X-API-Key", os.Getenv("ORBIT_API_KEY"))
    	req.Header.Set("Content-Type", "application/json")
    	http.DefaultClient.Do(req)
    }
    ```

    ```php PHP theme={null}
    <?php
    $ch = curl_init('https://api.orbit.devotel.io/api/v1/voice/queues');
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST');
    curl_setopt($ch, CURLOPT_HTTPHEADER, [
      'X-API-Key: ' . getenv('ORBIT_API_KEY'),
      'Content-Type: application/json',
    ]);
    curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode([
      'name' => 'Support Tier 1', 'routingStrategy' => 'longest-idle',
      'maxWaitSeconds' => 240, 'overflowAction' => 'voicemail',
    ]));
    echo curl_exec($ch);
    ```
  </CodeGroup>
</RequestExample>

### List / Update / Delete

`GET /api/v1/voice/queues`

`PUT /api/v1/voice/queues/{id}`

`DELETE /api/v1/voice/queues/{id}`

<RequestExample>
  <CodeGroup>
    ```bash cURL theme={null}
    # List
    curl -X GET "https://api.orbit.devotel.io/api/v1/voice/queues" \
      -H "X-API-Key: dv_live_sk_your_key_here"
    # Update
    curl -X PUT "https://api.orbit.devotel.io/api/v1/voice/queues/queue_abc" \
      -H "X-API-Key: dv_live_sk_your_key_here" \
      -H "Content-Type: application/json" \
      -d '{ "maxWaitSeconds": 180 }'
    # Delete
    curl -X DELETE "https://api.orbit.devotel.io/api/v1/voice/queues/queue_abc" \
      -H "X-API-Key: dv_live_sk_your_key_here"
    ```

    ```typescript Node.js theme={null}
    await orbit.voice.queues.list()
    await orbit.voice.queues.update('queue_abc', { maxWaitSeconds: 180 })
    await orbit.voice.queues.delete('queue_abc')
    ```

    ```python Python theme={null}
    import os, requests
    h = {"X-API-Key": os.environ["ORBIT_API_KEY"]}
    requests.get("https://api.orbit.devotel.io/api/v1/voice/queues", headers=h)
    requests.put("https://api.orbit.devotel.io/api/v1/voice/queues/queue_abc",
                 headers={**h, "Content-Type": "application/json"}, json={"maxWaitSeconds": 180})
    requests.delete("https://api.orbit.devotel.io/api/v1/voice/queues/queue_abc", headers=h)
    ```

    ```go Go theme={null}
    package main

    import (
    	"net/http"
    	"os"
    )

    func main() {
    	req, _ := http.NewRequest("GET", "https://api.orbit.devotel.io/api/v1/voice/queues", nil)
    	req.Header.Set("X-API-Key", os.Getenv("ORBIT_API_KEY"))
    	http.DefaultClient.Do(req)
    }
    ```

    ```php PHP theme={null}
    <?php
    $ch = curl_init('https://api.orbit.devotel.io/api/v1/voice/queues');
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($ch, CURLOPT_HTTPHEADER, ['X-API-Key: ' . getenv('ORBIT_API_KEY')]);
    echo curl_exec($ch);
    ```
  </CodeGroup>
</RequestExample>

### Enqueue a Call

`POST /api/v1/voice/queues/{id}/enqueue`

<ParamField body="callControlId" type="string" required>
  Call control ID of the live call to add to the queue.
</ParamField>

<ParamField body="callerNumber" type="string" required>
  Caller's number in E.164 format.
</ParamField>

<ParamField body="priority" type="integer" default="10">
  Routing priority from `0` to `99`. Lower wins — `0` is the highest
  priority and routes ahead of everyone else. Use for VIP customers and
  escalations.
</ParamField>

<ParamField body="requiredSkills" type="string[]">
  Skill tags the answering agent must have. Only agents with every listed
  skill are eligible to take the call. Defaults to no skill requirement.
</ParamField>

<RequestExample>
  <CodeGroup>
    ```bash cURL theme={null}
    curl -X POST "https://api.orbit.devotel.io/api/v1/voice/queues/queue_abc/enqueue" \
      -H "X-API-Key: dv_live_sk_your_key_here" \
      -H "Content-Type: application/json" \
      -d '{ "callControlId": "cc_abc123", "callerNumber": "+14155550123", "priority": 5, "requiredSkills": ["billing"] }'
    ```

    ```typescript Node.js theme={null}
    await orbit.voice.queues.enqueue('queue_abc', { callControlId: 'cc_abc123', callerNumber: '+14155550123', priority: 5, requiredSkills: ['billing'] })
    ```

    ```python Python theme={null}
    import os, requests
    headers = {"X-API-Key": os.environ["ORBIT_API_KEY"], "Content-Type": "application/json"}
    r = requests.post("https://api.orbit.devotel.io/api/v1/voice/queues/queue_abc/enqueue",
                      headers=headers, json={"callControlId": "cc_abc123", "callerNumber": "+14155550123", "priority": 5, "requiredSkills": ["billing"]})
    print(r.json())
    ```

    ```go Go theme={null}
    package main

    import (
    	"bytes"
    	"net/http"
    	"os"
    )

    func main() {
    	req, _ := http.NewRequest("POST",
    		"https://api.orbit.devotel.io/api/v1/voice/queues/queue_abc/enqueue",
    		bytes.NewBuffer([]byte(`{"callControlId":"cc_abc123","callerNumber":"+14155550123","priority":5,"requiredSkills":["billing"]}`)))
    	req.Header.Set("X-API-Key", os.Getenv("ORBIT_API_KEY"))
    	req.Header.Set("Content-Type", "application/json")
    	http.DefaultClient.Do(req)
    }
    ```

    ```php PHP theme={null}
    <?php
    $ch = curl_init('https://api.orbit.devotel.io/api/v1/voice/queues/queue_abc/enqueue');
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST');
    curl_setopt($ch, CURLOPT_HTTPHEADER, [
      'X-API-Key: ' . getenv('ORBIT_API_KEY'),
      'Content-Type: application/json',
    ]);
    curl_setopt($ch, CURLOPT_POSTFIELDS, '{"callControlId":"cc_abc123","callerNumber":"+14155550123","priority":5,"requiredSkills":["billing"]}');
    echo curl_exec($ch);
    ```
  </CodeGroup>
</RequestExample>

### Queue Stats

`GET /api/v1/voice/queues/{id}/stats`

Returns live counts: `waiting`, `connected`, `abandoned_24h`, `avg_wait_seconds_24h`, plus per-agent presence.

<RequestExample>
  <CodeGroup>
    ```bash cURL theme={null}
    curl -X GET "https://api.orbit.devotel.io/api/v1/voice/queues/queue_abc/stats" \
      -H "X-API-Key: dv_live_sk_your_key_here"
    ```

    ```typescript Node.js theme={null}
    const stats = await orbit.voice.queues.stats('queue_abc')
    console.log(stats.data.waiting)
    ```

    ```python Python theme={null}
    import os, requests
    headers = {"X-API-Key": os.environ["ORBIT_API_KEY"]}
    r = requests.get("https://api.orbit.devotel.io/api/v1/voice/queues/queue_abc/stats", headers=headers)
    print(r.json())
    ```

    ```go Go theme={null}
    package main

    import (
    	"net/http"
    	"os"
    )

    func main() {
    	req, _ := http.NewRequest("GET",
    		"https://api.orbit.devotel.io/api/v1/voice/queues/queue_abc/stats", nil)
    	req.Header.Set("X-API-Key", os.Getenv("ORBIT_API_KEY"))
    	http.DefaultClient.Do(req)
    }
    ```

    ```php PHP theme={null}
    <?php
    $ch = curl_init('https://api.orbit.devotel.io/api/v1/voice/queues/queue_abc/stats');
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($ch, CURLOPT_HTTPHEADER, ['X-API-Key: ' . getenv('ORBIT_API_KEY')]);
    echo curl_exec($ch);
    ```
  </CodeGroup>
</RequestExample>

### Set Agent Status

`POST /api/v1/voice/agents/{id}/status`

<ParamField body="status" type="string" required>
  Agent presence: `available`, `busy`, `away`, `offline`.
</ParamField>

<ParamField body="reason_code" type="string">
  Required when `status` is `away` (for example `lunch`, `break`, `training`).
  Lowercase ASCII letters, digits, `_` or `-`, up to 64 characters.
</ParamField>

Drives queue routing — `available` agents are eligible for the next call.

<RequestExample>
  <CodeGroup>
    ```bash cURL theme={null}
    curl -X POST "https://api.orbit.devotel.io/api/v1/voice/agents/agt_jane/status" \
      -H "X-API-Key: dv_live_sk_your_key_here" \
      -H "Content-Type: application/json" \
      -d '{ "status": "available" }'
    ```

    ```typescript Node.js theme={null}
    await orbit.voice.agents.setStatus('agt_jane', { status: 'available' })
    ```

    ```python Python theme={null}
    import os, requests
    headers = {"X-API-Key": os.environ["ORBIT_API_KEY"], "Content-Type": "application/json"}
    r = requests.post("https://api.orbit.devotel.io/api/v1/voice/agents/agt_jane/status",
                      headers=headers, json={"status": "available"})
    print(r.json())
    ```

    ```go Go theme={null}
    package main

    import (
    	"bytes"
    	"net/http"
    	"os"
    )

    func main() {
    	req, _ := http.NewRequest("POST",
    		"https://api.orbit.devotel.io/api/v1/voice/agents/agt_jane/status",
    		bytes.NewBuffer([]byte(`{"status":"available"}`)))
    	req.Header.Set("X-API-Key", os.Getenv("ORBIT_API_KEY"))
    	req.Header.Set("Content-Type", "application/json")
    	http.DefaultClient.Do(req)
    }
    ```

    ```php PHP theme={null}
    <?php
    $ch = curl_init('https://api.orbit.devotel.io/api/v1/voice/agents/agt_jane/status');
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST');
    curl_setopt($ch, CURLOPT_HTTPHEADER, [
      'X-API-Key: ' . getenv('ORBIT_API_KEY'),
      'Content-Type: application/json',
    ]);
    curl_setopt($ch, CURLOPT_POSTFIELDS, '{"status":"available"}');
    echo curl_exec($ch);
    ```
  </CodeGroup>
</RequestExample>

***

## Ring Groups

A ring group rings multiple destinations (SIP usernames, PSTN numbers, or other ring groups) on a single inbound number. Use for "ring all sales reps until someone answers" patterns, plus hunt-group walks and nested team-of-teams structures.

### Create / List / Get / Delete

`POST /api/v1/voice/ring-groups`

<ParamField body="name" type="string" required>
  Unique ring group name within the organization (max 100 chars).
</ParamField>

<ParamField body="strategy" type="string" default="simultaneous">
  Ring strategy. Implemented today: `simultaneous`, `sequential`,
  `round_robin`, `longest_idle`, `fewest_calls`. See the Strategies
  section below for how each one selects members.
</ParamField>

<ParamField body="members" type="object[]" required>
  Array of `{ kind, value }` entries. `kind` is one of
  `"sip_username"` (references a row in `tenant_sip_credentials`),
  `"pstn"` (an E.164-ish digits-only number, `^\+?[0-9]{7,20}$`), or
  `"ring_group"` (id of another ring group in the same org —
  cycle-checked at save time). Min 1, max 100 members per group.
</ParamField>

<ParamField body="ringTimeoutSec" type="integer" default="30">
  Per-step ring timeout in seconds (5-300). For `simultaneous` it's
  the overall ring duration before the actionHook fires; for
  `sequential` it's the per-username ring duration.
</ParamField>

`GET /api/v1/voice/ring-groups`

`GET /api/v1/voice/ring-groups/{id}`

`DELETE /api/v1/voice/ring-groups/{id}`

#### Strategies

* **`simultaneous`** — fork the INVITE to every leaf in parallel; first answer wins. PSTN leaves are dialed in parallel with SIP usernames.
* **`sequential`** — walk SIP usernames in array order, advancing on no-answer via `/jambonz/lookup-next`. PSTN leaves are appended after the SIP walk so "try Alice's desk, then Bob's desk, then ring my mobile" works without a parallel fork.
* **`round_robin`** — Redis-pinned cursor at `{devotel}:ringgroup:rr:<groupId>` rotates picks across members one at a time. One target per call so per-call billing aligns with the answerOnBridge contract.
* **`longest_idle`** — Redis hash at `{devotel}:ringgroup:li:<groupId>` records per-member last-selected timestamp. Picker chooses the longest-idle member; never-selected members beat any timestamped member; alphabetical tie-break gives deterministic convergence across replicas.
* **`fewest_calls`** — Redis hash at `{devotel}:ringgroup:fc:<groupId>` tracks per-member calls handled. Picker chooses the member with the fewest handled calls; never-selected members count as zero and beat any member with a positive count; alphabetical tie-break gives deterministic convergence across replicas. The count is incremented at dial-time when the `dial` verb is emitted to the picked member. One target per call so per-call billing aligns with the answerOnBridge contract.

Wiring a ring group to an inbound number: save the route via `PUT /api/v1/numbers/{phoneNumber}/routing` with `type: "ring_group"`, `config: {}`, and `ring_group_id: "<group_id>"` — the group identity lives on the route row, not in `config`.

<RequestExample>
  <CodeGroup>
    ```bash cURL theme={null}
    # Create
    curl -X POST "https://api.orbit.devotel.io/api/v1/voice/ring-groups" \
      -H "X-API-Key: dv_live_sk_your_key_here" \
      -H "Content-Type: application/json" \
      -d '{
      "name": "Sales All-Rings",
      "strategy": "simultaneous",
      "ringTimeoutSec": 30,
      "members": [
        { "kind": "sip_username", "value": "alice" },
        { "kind": "sip_username", "value": "bob" },
        { "kind": "pstn",         "value": "+14155557890" }
      ]
    }'
    # List / Get / Delete
    curl -X GET "https://api.orbit.devotel.io/api/v1/voice/ring-groups" -H "X-API-Key: $K"
    curl -X GET "https://api.orbit.devotel.io/api/v1/voice/ring-groups/rg_abc" -H "X-API-Key: $K"
    curl -X DELETE "https://api.orbit.devotel.io/api/v1/voice/ring-groups/rg_abc" -H "X-API-Key: $K"
    ```

    ```typescript Node.js theme={null}
    await orbit.voice.ringGroups.create({
      name: 'Sales All-Rings',
      strategy: 'simultaneous',
      ringTimeoutSec: 30,
      members: [
        { kind: 'sip_username', value: 'alice' },
        { kind: 'sip_username', value: 'bob' },
        { kind: 'pstn',         value: '+14155557890' },
      ],
    })
    await orbit.voice.ringGroups.list()
    await orbit.voice.ringGroups.get('rg_abc')
    await orbit.voice.ringGroups.delete('rg_abc')
    ```

    ```python Python theme={null}
    import os, requests
    headers = {"X-API-Key": os.environ["ORBIT_API_KEY"], "Content-Type": "application/json"}
    r = requests.post("https://api.orbit.devotel.io/api/v1/voice/ring-groups", headers=headers, json={
      "name": "Sales All-Rings",
      "strategy": "simultaneous",
      "ringTimeoutSec": 30,
      "members": [
        {"kind": "sip_username", "value": "alice"},
        {"kind": "pstn",         "value": "+14155557890"}
      ]
    })
    print(r.json())
    ```

    ```go Go theme={null}
    package main

    import (
    	"bytes"
    	"net/http"
    	"os"
    )

    func main() {
    	body := []byte(`{
      "name": "Sales All-Rings",
      "strategy": "simultaneous",
      "ringTimeoutSec": 30,
      "members": [{"kind":"sip_username","value":"alice"}]
    }`)
    	req, _ := http.NewRequest("POST", "https://api.orbit.devotel.io/api/v1/voice/ring-groups", bytes.NewBuffer(body))
    	req.Header.Set("X-API-Key", os.Getenv("ORBIT_API_KEY"))
    	req.Header.Set("Content-Type", "application/json")
    	http.DefaultClient.Do(req)
    }
    ```

    ```php PHP theme={null}
    <?php
    $ch = curl_init('https://api.orbit.devotel.io/api/v1/voice/ring-groups');
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST');
    curl_setopt($ch, CURLOPT_HTTPHEADER, [
      'X-API-Key: ' . getenv('ORBIT_API_KEY'),
      'Content-Type: application/json',
    ]);
    curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode([
      'name' => 'Sales All-Rings',
      'strategy' => 'simultaneous',
      'ringTimeoutSec' => 30,
      'members' => [['kind' => 'sip_username', 'value' => 'alice']],
    ]));
    echo curl_exec($ch);
    ```
  </CodeGroup>
</RequestExample>

***

## Paging Groups

A paging group broadcasts a one-way (or two-way) announcement to a set of deskphones and softphone users at once — the "page the warehouse" / overhead-intercom pattern that replaces a legacy PBX paging system. Members are resolved at page-time into two delivery channels: registered SIP deskphones (rung directly via an internal user-to-user INVITE) and in-app softphone users.

<Info>
  Paging legs are internal `user:` targets on Orbit's own SIP edge — they never traverse a carrier trunk or reach the PSTN.
</Info>

### Create / List / Get / Update / Delete

`POST /api/v1/voice/paging-groups`

<ParamField body="name" type="string" required>
  Unique paging group name within the organization (max 100 chars). A duplicate name returns `409`.
</ParamField>

<ParamField body="description" type="string">
  Optional human-readable description (max 500 chars).
</ParamField>

<ParamField body="mode" type="string" default="simplex">
  Page mode. One of `simplex` (one-way announcement) or `duplex` (two-way intercom). Until the live two-way conference bridge lands, a `duplex` page still rings every deskphone and plays the announcement (a functional one-way notification), so no member is dropped.
</ParamField>

<ParamField body="members" type="object[]" default="[]">
  Array of `{ kind, value }` entries. `kind` is one of `"sip_username"` (a registered softphone/deskphone in `tenant_sip_credentials`) or `"user_id"` (an organization user whose softphone receives an in-app page). Max 200 members per group.
</ParamField>

<ParamField body="maxAnnouncementSeconds" type="integer" default="30">
  Maximum announcement duration in seconds (5-300).
</ParamField>

<ParamField body="chimeUrl" type="string">
  Optional URL of an audio file played as an attention chime before the announcement (max 2048 chars).
</ParamField>

`GET /api/v1/voice/paging-groups` — cursor-paginated list (`?cursor=&limit=` up to 100). Each item carries `memberCount`.

`GET /api/v1/voice/paging-groups/{id}`

`PATCH /api/v1/voice/paging-groups/{id}` — partial update; any subset of the create fields.

`DELETE /api/v1/voice/paging-groups/{id}` — hard delete (`204`; `404` if unknown).

### Trigger a Page

`POST /api/v1/voice/paging-groups/{id}/page`

<ParamField body="message" type="string">
  Optional one-shot message read out via TTS instead of the pager's live mic (1-500 chars).
</ParamField>

<ParamField body="modeOverride" type="string">
  Override the group's configured mode for this page only (`simplex` or `duplex`) — e.g. force `simplex` on a `duplex` group for an emergency announcement.
</ParamField>

The response reports both delivery channels so the UI can render "Paged N softphone users + M deskphones (K legs failed)":

```json theme={null}
{
  "pagingGroupId": "pagingGroup_abc",
  "name": "Warehouse Floor",
  "mode": "simplex",
  "maxAnnouncementSeconds": 30,
  "recipientsResolved": 3,
  "recipientsNotified": 3,
  "sipDeskphonesTargeted": 8,
  "sipDeskphonesPaged": 7,
  "sipDeskphonesFailed": 1
}
```

<RequestExample>
  <CodeGroup>
    ```bash cURL theme={null}
    # Create
    curl -X POST "https://api.orbit.devotel.io/api/v1/voice/paging-groups" \
      -H "X-API-Key: dv_live_sk_your_key_here" \
      -H "Content-Type: application/json" \
      -d '{
      "name": "Warehouse Floor",
      "mode": "simplex",
      "maxAnnouncementSeconds": 30,
      "members": [
        { "kind": "sip_username", "value": "dock-phone-1" },
        { "kind": "user_id",      "value": "user_abc" }
      ]
    }'
    # List / Get / Update / Delete
    curl -X GET "https://api.orbit.devotel.io/api/v1/voice/paging-groups" -H "X-API-Key: $K"
    curl -X GET "https://api.orbit.devotel.io/api/v1/voice/paging-groups/pagingGroup_abc" -H "X-API-Key: $K"
    curl -X PATCH "https://api.orbit.devotel.io/api/v1/voice/paging-groups/pagingGroup_abc" \
      -H "X-API-Key: $K" -H "Content-Type: application/json" \
      -d '{ "name": "Warehouse Floor (Day Shift)" }'
    curl -X DELETE "https://api.orbit.devotel.io/api/v1/voice/paging-groups/pagingGroup_abc" -H "X-API-Key: $K"
    # Trigger a page
    curl -X POST "https://api.orbit.devotel.io/api/v1/voice/paging-groups/pagingGroup_abc/page" \
      -H "X-API-Key: $K" -H "Content-Type: application/json" \
      -d '{ "message": "Shipment arriving at dock 3" }'
    ```

    ```typescript Node.js theme={null}
    await orbit.voice.pagingGroups.create({
      name: 'Warehouse Floor',
      mode: 'simplex',
      maxAnnouncementSeconds: 30,
      members: [
        { kind: 'sip_username', value: 'dock-phone-1' },
        { kind: 'user_id',      value: 'user_abc' },
      ],
    })
    await orbit.voice.pagingGroups.list()
    await orbit.voice.pagingGroups.get('pagingGroup_abc')
    await orbit.voice.pagingGroups.update('pagingGroup_abc', { name: 'Warehouse Floor (Day Shift)' })
    await orbit.voice.pagingGroups.delete('pagingGroup_abc')
    await orbit.voice.pagingGroups.page('pagingGroup_abc', { message: 'Shipment arriving at dock 3' })
    ```

    ```python Python theme={null}
    import os, requests
    headers = {"X-API-Key": os.environ["ORBIT_API_KEY"], "Content-Type": "application/json"}
    r = requests.post("https://api.orbit.devotel.io/api/v1/voice/paging-groups", headers=headers, json={
      "name": "Warehouse Floor",
      "mode": "simplex",
      "maxAnnouncementSeconds": 30,
      "members": [
        {"kind": "sip_username", "value": "dock-phone-1"},
        {"kind": "user_id",      "value": "user_abc"}
      ]
    })
    print(r.json())
    # Trigger a page
    requests.post(
      "https://api.orbit.devotel.io/api/v1/voice/paging-groups/pagingGroup_abc/page",
      headers=headers, json={"message": "Shipment arriving at dock 3"})
    ```
  </CodeGroup>
</RequestExample>

***

## Shared Lines

Shared Line Appearance (SLA) mirrors one "line" (a primary extension) onto several physical phones — the receptionist / exec-assistant pattern where an inbound call rings every member and any member can answer or grab a parked call. This endpoint family manages the line configuration and member roster; the inbound call-fork that makes a call ring multiple phones is handled by the voice call plane.

<Info>
  Shared lines are inbound fork-and-park only — no outbound termination is wired here.
</Info>

### Create / List / Get / Update / Delete

`POST /api/v1/voice/shared-lines`

<ParamField body="name" type="string" required>
  Unique shared line name within the organization (max 100 chars). A duplicate name or extension returns `409`.
</ParamField>

<ParamField body="description" type="string">
  Optional description (max 500 chars).
</ParamField>

<ParamField body="primaryExtension" type="string" required>
  The primary extension mirrored onto the member phones (max 32 chars). Unique within the organization.
</ParamField>

<ParamField body="status" type="string" default="active">
  Line status. One of `active` or `inactive`.
</ParamField>

`GET /api/v1/voice/shared-lines` — cursor-paginated list (`?cursor=&limit=` up to 100). Each item carries `memberCount`.

`GET /api/v1/voice/shared-lines/{id}` — returns the line plus its `members[]` ordered by `sortOrder` then `createdAt`.

`PATCH /api/v1/voice/shared-lines/{id}` — partial update.

`DELETE /api/v1/voice/shared-lines/{id}` — hard delete; member rows cascade.

### Members

`POST /api/v1/voice/shared-lines/{id}/members`

<ParamField body="memberKind" type="string" default="sip_extension">
  One of `sip_extension` (a registered phone in `tenant_sip_credentials`, existence-checked org-scoped at add — an unknown username returns `422`) or `user_id` (an organization user whose softphone is resolved by the fork layer at call-time).
</ParamField>

<ParamField body="memberValue" type="string" required>
  The SIP username or user id, depending on `memberKind` (max 128 chars). Adding the same value twice returns `409`.
</ParamField>

<ParamField body="canGrab" type="boolean" default="true">
  Whether this member may grab (pick up) a call already ringing/parked on the line.
</ParamField>

<ParamField body="sortOrder" type="integer">
  Optional button position (0-999). When omitted, the next sequential position is assigned automatically.
</ParamField>

A line is capped at 50 members; adding past the cap returns `409`.

`DELETE /api/v1/voice/shared-lines/{id}/members/{memberId}` — remove a member (`204`; `404` if unknown).

<RequestExample>
  <CodeGroup>
    ```bash cURL theme={null}
    # Create a line
    curl -X POST "https://api.orbit.devotel.io/api/v1/voice/shared-lines" \
      -H "X-API-Key: dv_live_sk_your_key_here" \
      -H "Content-Type: application/json" \
      -d '{
      "name": "Reception",
      "primaryExtension": "100",
      "status": "active"
    }'
    # List / Get / Update / Delete
    curl -X GET "https://api.orbit.devotel.io/api/v1/voice/shared-lines" -H "X-API-Key: $K"
    curl -X GET "https://api.orbit.devotel.io/api/v1/voice/shared-lines/sharedLine_abc" -H "X-API-Key: $K"
    curl -X PATCH "https://api.orbit.devotel.io/api/v1/voice/shared-lines/sharedLine_abc" \
      -H "X-API-Key: $K" -H "Content-Type: application/json" -d '{ "status": "inactive" }'
    curl -X DELETE "https://api.orbit.devotel.io/api/v1/voice/shared-lines/sharedLine_abc" -H "X-API-Key: $K"
    # Add / remove a member
    curl -X POST "https://api.orbit.devotel.io/api/v1/voice/shared-lines/sharedLine_abc/members" \
      -H "X-API-Key: $K" -H "Content-Type: application/json" \
      -d '{ "memberKind": "sip_extension", "memberValue": "front-desk-1", "canGrab": true }'
    curl -X DELETE "https://api.orbit.devotel.io/api/v1/voice/shared-lines/sharedLine_abc/members/sharedLineMember_xyz" \
      -H "X-API-Key: $K"
    ```

    ```typescript Node.js theme={null}
    await orbit.voice.sharedLines.create({
      name: 'Reception',
      primaryExtension: '100',
      status: 'active',
    })
    await orbit.voice.sharedLines.list()
    await orbit.voice.sharedLines.get('sharedLine_abc')
    await orbit.voice.sharedLines.update('sharedLine_abc', { status: 'inactive' })
    await orbit.voice.sharedLines.delete('sharedLine_abc')
    await orbit.voice.sharedLines.members.add('sharedLine_abc', {
      memberKind: 'sip_extension',
      memberValue: 'front-desk-1',
      canGrab: true,
    })
    await orbit.voice.sharedLines.members.remove('sharedLine_abc', 'sharedLineMember_xyz')
    ```

    ```python Python theme={null}
    import os, requests
    headers = {"X-API-Key": os.environ["ORBIT_API_KEY"], "Content-Type": "application/json"}
    r = requests.post("https://api.orbit.devotel.io/api/v1/voice/shared-lines", headers=headers, json={
      "name": "Reception",
      "primaryExtension": "100",
      "status": "active"
    })
    line_id = r.json()["data"]["id"]
    requests.post(
      f"https://api.orbit.devotel.io/api/v1/voice/shared-lines/{line_id}/members",
      headers=headers,
      json={"memberKind": "sip_extension", "memberValue": "front-desk-1", "canGrab": True})
    ```
  </CodeGroup>
</RequestExample>

***

## Devices

Register physical desk phones for zero-touch provisioning. Each device pairs a MAC address with a vendor and (optionally) a SIP credential; the response returns a `provisioningUrl` your IT team plugs into the vendor redirect server (Polycom ZTP / Yealink RPS / Cisco EDOS / Grandstream GDMS). On first boot the phone fetches its rendered config and registers against Orbit's SIP edge.

<Info>
  Provisioning points the phone at Orbit's SIP edge (`sip.orbit.devotel.io`), which routes through to the Devotel softswitch — phones are never provisioned to register directly at an upstream carrier.
</Info>

### Create / List / Get / Update / Delete

`POST /api/v1/voice/devices`

<ParamField body="macAddress" type="string" required>
  The phone's MAC address (12-17 chars, with or without separators). Normalised server-side; a MAC already registered returns `409`.
</ParamField>

<ParamField body="vendor" type="string" required>
  Device vendor. One of `polycom`, `yealink`, `cisco`, or `grandstream`.
</ParamField>

<ParamField body="model" type="string">
  Optional model identifier (max 64 chars).
</ParamField>

<ParamField body="sipCredentialId" type="string">
  Optional id of a SIP credential in your organization to bind to the phone. Must belong to your organization (a foreign id returns `404`). Until set, the phone has nothing to register with.
</ParamField>

<ParamField body="label" type="string">
  Optional operator-facing label (max 64 chars).
</ParamField>

<ParamField body="notes" type="string">
  Optional free-text notes (max 1000 chars).
</ParamField>

<ParamField body="claimExpiresAt" type="string">
  ISO 8601 timestamp after which an un-booted device row auto-expires. Defaults to 30 days from creation; pass `null` for no expiry (long-tail fleet rollouts).
</ParamField>

`GET /api/v1/voice/devices` — list every device in your organization.

`GET /api/v1/voice/devices/{id}`

`PATCH /api/v1/voice/devices/{id}` — update `label`, `sipCredentialId`, `model`, `notes`, `disabled`, or `claimExpiresAt`. A field present with value `null` clears the column.

`DELETE /api/v1/voice/devices/{id}` — soft delete.

<RequestExample>
  <CodeGroup>
    ```bash cURL theme={null}
    # Register a device
    curl -X POST "https://api.orbit.devotel.io/api/v1/voice/devices" \
      -H "X-API-Key: dv_live_sk_your_key_here" \
      -H "Content-Type: application/json" \
      -d '{
      "macAddress": "aabbccddeeff",
      "vendor": "yealink",
      "model": "T54W",
      "sipCredentialId": "sipcred_abc",
      "label": "Front desk"
    }'
    # List / Get / Update / Delete
    curl -X GET "https://api.orbit.devotel.io/api/v1/voice/devices" -H "X-API-Key: $K"
    curl -X GET "https://api.orbit.devotel.io/api/v1/voice/devices/sipdev_abc" -H "X-API-Key: $K"
    curl -X PATCH "https://api.orbit.devotel.io/api/v1/voice/devices/sipdev_abc" \
      -H "X-API-Key: $K" -H "Content-Type: application/json" -d '{ "disabled": true }'
    curl -X DELETE "https://api.orbit.devotel.io/api/v1/voice/devices/sipdev_abc" -H "X-API-Key: $K"
    ```

    ```typescript Node.js theme={null}
    const device = await orbit.voice.devices.create({
      macAddress: 'aabbccddeeff',
      vendor: 'yealink',
      model: 'T54W',
      sipCredentialId: 'sipcred_abc',
      label: 'Front desk',
    })
    // Paste device.provisioningUrl into the vendor redirect server.
    await orbit.voice.devices.list()
    await orbit.voice.devices.get('sipdev_abc')
    await orbit.voice.devices.update('sipdev_abc', { disabled: true })
    await orbit.voice.devices.delete('sipdev_abc')
    ```

    ```python Python theme={null}
    import os, requests
    headers = {"X-API-Key": os.environ["ORBIT_API_KEY"], "Content-Type": "application/json"}
    r = requests.post("https://api.orbit.devotel.io/api/v1/voice/devices", headers=headers, json={
      "macAddress": "aabbccddeeff",
      "vendor": "yealink",
      "model": "T54W",
      "sipCredentialId": "sipcred_abc",
      "label": "Front desk"
    })
    print(r.json()["data"]["provisioningUrl"])
    ```
  </CodeGroup>
</RequestExample>

***

## Call Flip / Call Pull

Move a live call between devices without dropping it — the mobile-↔-desktop continuity pattern. The agent flips a call on one registered device (issuing a short-lived handoff token) and pulls it onto another device (their mobile soft-client, another desk phone) by claiming that token. This API manages the handoff ledger; the actual SIP re-INVITE that moves the leg is performed by the voice call plane.

<Info>
  Per Orbit's outbound-termination invariant, the moved (mobile-terminated) leg is re-INVITEd via the Devotel wholesale softswitch — never an upstream carrier's call-control API.
</Info>

### Flip a Call

`POST /api/v1/voice/call-flip`

<ParamField body="callSid" type="string" required>
  SID of the live call being flipped (1-128 chars).
</ParamField>

<ParamField body="targetDeviceUsername" type="string">
  Optional SIP username (from `tenant_sip_credentials.username`) that the call is pinned to. When omitted, any registered device in the organization may pull the call; when set, only that device may pull (otherwise the pull returns `403`).
</ParamField>

<ParamField body="callLogId" type="string">
  Optional reference to a `call_logs.id` for join-time lookup.
</ParamField>

<ParamField body="remotePartyNumber" type="string">
  Optional E.164 number of the remote leg, shown in the pending-flips list.
</ParamField>

<ParamField body="remotePartyName" type="string">
  Optional display name of the remote leg.
</ParamField>

Returns `201` with the handoff, including the full `token` (returned only on this call), a 4-digit `shortCode` for DTMF pulls (`*7` then the code on a legacy desk phone), and `expiresAt`. The token TTL is 120 seconds. Re-issuing a flip for the same `callSid` by the same user while one is active returns the existing token (idempotent).

### Pull a Call

`POST /api/v1/voice/call-flip/pull`

<ParamField body="token" type="string" required>
  The token returned by the flip endpoint (or the 4-digit short-code on the dial-code path).
</ParamField>

<ParamField body="pulledByDeviceUsername" type="string" required>
  The pulling device's SIP username. When the flip pinned a `targetDeviceUsername`, this must match.
</ParamField>

Returns `200` with the resolved `callSid` on success. A token that was already pulled or has expired returns `410`; a device mismatch returns `403`; an unknown token returns `404`.

### List / Cancel

`GET /api/v1/voice/call-flip` — list every live (un-pulled, un-expired) handoff in the organization. Tokens are never returned by this endpoint.

`POST /api/v1/voice/call-flip/{id}/cancel` — cancel an outstanding flip (the agent changed their mind). A subsequent pull then returns `410`. By default only the user who issued the flip may cancel it; owners, admins, and supervisors may cancel any flip in their organization.

<RequestExample>
  <CodeGroup>
    ```bash cURL theme={null}
    # Flip the live call (returns token + shortCode + expiresAt)
    curl -X POST "https://api.orbit.devotel.io/api/v1/voice/call-flip" \
      -H "X-API-Key: dv_live_sk_your_key_here" \
      -H "Content-Type: application/json" \
      -d '{ "callSid": "call_abc", "remotePartyNumber": "+14155557890" }'
    # Pull the call onto another device
    curl -X POST "https://api.orbit.devotel.io/api/v1/voice/call-flip/pull" \
      -H "X-API-Key: $K" -H "Content-Type: application/json" \
      -d '{ "token": "flip_…", "pulledByDeviceUsername": "alice-mobile" }'
    # List pending flips / cancel one
    curl -X GET "https://api.orbit.devotel.io/api/v1/voice/call-flip" -H "X-API-Key: $K"
    curl -X POST "https://api.orbit.devotel.io/api/v1/voice/call-flip/flip_…/cancel" -H "X-API-Key: $K"
    ```

    ```typescript Node.js theme={null}
    const flip = await orbit.voice.callFlip.flip({
      callSid: 'call_abc',
      remotePartyNumber: '+14155557890',
    })
    // flip.token + flip.shortCode + flip.expiresAt
    await orbit.voice.callFlip.pull({
      token: flip.token,
      pulledByDeviceUsername: 'alice-mobile',
    })
    await orbit.voice.callFlip.list()
    await orbit.voice.callFlip.cancel(flip.id)
    ```

    ```python Python theme={null}
    import os, requests
    headers = {"X-API-Key": os.environ["ORBIT_API_KEY"], "Content-Type": "application/json"}
    flip = requests.post("https://api.orbit.devotel.io/api/v1/voice/call-flip", headers=headers,
      json={"callSid": "call_abc", "remotePartyNumber": "+14155557890"}).json()["data"]
    requests.post("https://api.orbit.devotel.io/api/v1/voice/call-flip/pull", headers=headers,
      json={"token": flip["token"], "pulledByDeviceUsername": "alice-mobile"})
    ```
  </CodeGroup>
</RequestExample>

***

## Hot-Desking

Let agents sign in to a shared physical desk phone so it re-skins as their own extension for the duration of a shift, then falls back to the shared inbound pool on sign-out — the retail / healthcare / hybrid-office "hardware-first" pattern (Cisco Extension Mobility, RingCentral Hot Desking, 8x8 Shared Phone Desks).

There is **one active session per device and one per user**: signing in to a second device automatically signs you out of the first, and signing in to a device already bound to someone else displaces their session. Displaced sessions are returned in the sign-in response so the dashboard can show who was bumped.

### Sign In

`POST /api/v1/voice/hot-desking/sign-in`

<ParamField body="deviceSipUsername" type="string" required>
  SIP username of the shared desk phone — the same username you manage under [SIP Credentials](/api-reference/sip-credentials). Lowercase/uppercase letters, digits, `.`, `_` or `-`; 3–128 characters (for example `acme-shared-5F-2`).
</ParamField>

<ParamField body="deviceLabelSnapshot" type="string">
  Optional human-readable device label captured at sign-in time (max 120 chars) so the audit trail survives a later device rename.
</ParamField>

<ParamField body="expiresAt" type="string">
  Optional ISO-8601 scheduled auto-sign-out. Must be in the future and within 24 hours, otherwise the request returns `400`.
</ParamField>

Returns `201` with `{ session, displaced }` on a fresh bind, or `200` with the existing session when the same user re-signs in to the same device (idempotent no-op). A concurrent sign-in race returns `409`.

### Sign Out

`POST /api/v1/voice/hot-desking/sign-out`

Releases the calling user's active session and returns the device to the shared inbound pool. Returns `200` with the released session. A second sign-out returns `410` (with the prior release reason and timestamp) when you have a prior session, or `404` when you were never signed in.

### List Active Sessions

`GET /api/v1/voice/hot-desking` — list the organization's active (not signed-out) hot-desk sessions, newest first, capped at 200. Powers the dashboard's hot-desking board.

### My Session

`GET /api/v1/voice/hot-desking/me` — returns the calling user's active session, or `null` under `data` when not signed in to any device.

<RequestExample>
  <CodeGroup>
    ```bash cURL theme={null}
    # Sign in to a shared desk phone
    curl -X POST "https://api.orbit.devotel.io/api/v1/voice/hot-desking/sign-in" \
      -H "X-API-Key: dv_live_sk_your_key_here" \
      -H "Content-Type: application/json" \
      -d '{ "deviceSipUsername": "acme-shared-5F-2" }'
    # Who am I signed in as?
    curl "https://api.orbit.devotel.io/api/v1/voice/hot-desking/me" -H "X-API-Key: $K"
    # List active sessions for the org
    curl "https://api.orbit.devotel.io/api/v1/voice/hot-desking" -H "X-API-Key: $K"
    # Sign out
    curl -X POST "https://api.orbit.devotel.io/api/v1/voice/hot-desking/sign-out" -H "X-API-Key: $K"
    ```

    ```typescript Node.js theme={null}
    const signIn = await orbit.voice.hotDesking.signIn({
      deviceSipUsername: 'acme-shared-5F-2',
    })
    // signIn.session + signIn.displaced
    await orbit.voice.hotDesking.me()
    await orbit.voice.hotDesking.list()
    await orbit.voice.hotDesking.signOut()
    ```

    ```python Python theme={null}
    import os, requests
    headers = {"X-API-Key": os.environ["ORBIT_API_KEY"], "Content-Type": "application/json"}
    sign_in = requests.post("https://api.orbit.devotel.io/api/v1/voice/hot-desking/sign-in",
      headers=headers, json={"deviceSipUsername": "acme-shared-5F-2"}).json()["data"]
    requests.post("https://api.orbit.devotel.io/api/v1/voice/hot-desking/sign-out", headers=headers)
    ```
  </CodeGroup>
</RequestExample>

<ResponseExample>
  ```json 201 theme={null}
  {
    "data": {
      "session": {
        "id": "hds_0123456789abcdef0123456789abcdef",
        "deviceSipUsername": "acme-shared-5F-2",
        "userId": "usr_abc123",
        "deviceLabelSnapshot": "Front Desk 5F",
        "userDisplayNameSnapshot": null,
        "signedInAt": "2026-06-28T09:00:00Z",
        "expiresAt": null,
        "signedOutAt": null,
        "releaseReason": null,
        "activeForSeconds": 0
      },
      "displaced": []
    }
  }
  ```
</ResponseExample>

***

## Hoteling

Hot-desking lets a user claim a shared desk phone **now**; hoteling adds the advance-booking layer on top — an agent or knowledge-worker reserves a specific desk/extension for a **future** date/time window, the system prevents double-booking, and a no-show or elapsed reservation is auto-released so a desk is never dead-locked by an absent booker (the Cisco / RingCentral / 8x8 "book a desk" pattern). These endpoints back the reservation calendar shown under **Voice → Hot-Desking**.

A booking is rejected with `409 HOTELING_DOUBLE_BOOKED` when its `[startsAt, endsAt)` window overlaps an existing active reservation for the same device; the conflicting reservation is returned under `error.details.conflict`.

### Book a Reservation

`POST /api/v1/voice/hoteling/reservations`

<ParamField body="deviceSipUsername" type="string" required>
  SIP username of the shared desk phone being booked — the same username you manage under [SIP Credentials](/api-reference/sip-credentials). 3–128 characters of letters, digits, `.`, `_` or `-` (for example `acme-shared-5F-2`).
</ParamField>

<ParamField body="startsAt" type="string" required>
  ISO-8601 inclusive start of the booked window (for example `2026-07-20T09:00:00Z`).
</ParamField>

<ParamField body="endsAt" type="string" required>
  ISO-8601 exclusive end of the booked window. Must be after `startsAt`, in the future, and at most 24 hours after `startsAt`; `startsAt` itself may be at most 90 days ahead.
</ParamField>

<ParamField body="deviceLabelSnapshot" type="string">
  Optional human-readable device label captured at booking time (max 120 chars) so the audit trail survives a later device rename.
</ParamField>

<ParamField body="notes" type="string">
  Optional free-text note shown on the booking calendar (max 500 chars).
</ParamField>

Returns `201` with the created reservation, or `409 HOTELING_DOUBLE_BOOKED` when the window overlaps an existing reservation for the same desk.

### List Reservations

`GET /api/v1/voice/hoteling/reservations`

<ParamField query="scope" type="string" default="me">
  `me` returns the caller's reservations; `team` returns the whole organization's.
</ParamField>

<ParamField query="from" type="string">
  Optional ISO-8601 lower bound — only reservations ending after this instant are returned.
</ParamField>

<ParamField query="to" type="string">
  Optional ISO-8601 upper bound — only reservations starting before this instant are returned.
</ParamField>

<ParamField query="deviceSipUsername" type="string">
  Optional filter to a single shared desk.
</ParamField>

Returns the matching reservations ordered by window start, capped at 200. Elapsed windows are auto-released before the list is read.

### Cancel a Reservation

`DELETE /api/v1/voice/hoteling/reservations/{id}`

Cancels an active reservation. The booker can cancel their own; `owner`/`admin` can cancel any. Returns `410` when the reservation is already cancelled, fulfilled, or expired, and `404` when it does not exist for your organization.

### Release Elapsed Reservations

`POST /api/v1/voice/hoteling/reservations/sweep`

`owner`/`admin` only. Flips any reservation whose window has fully elapsed to `expired`, freeing the desk. This also runs automatically on every reservation read/book — call it explicitly only to force an immediate sweep. Returns `{ "released": <count> }`.

<RequestExample>
  <CodeGroup>
    ```bash cURL theme={null}
    K="dv_live_sk_your_key_here"
    # Book a desk for a future window
    curl -X POST "https://api.orbit.devotel.io/api/v1/voice/hoteling/reservations" \
      -H "X-API-Key: $K" \
      -H "Content-Type: application/json" \
      -d '{
      "deviceSipUsername": "acme-shared-5F-2",
      "startsAt": "2026-07-20T09:00:00Z",
      "endsAt": "2026-07-20T17:00:00Z"
    }'
    # List my upcoming reservations
    curl "https://api.orbit.devotel.io/api/v1/voice/hoteling/reservations?scope=me" -H "X-API-Key: $K"
    # Cancel a reservation
    curl -X DELETE "https://api.orbit.devotel.io/api/v1/voice/hoteling/reservations/hdr_0123456789abcdef0123456789abcdef" -H "X-API-Key: $K"
    # Release elapsed / no-show reservations (owner/admin)
    curl -X POST "https://api.orbit.devotel.io/api/v1/voice/hoteling/reservations/sweep" -H "X-API-Key: $K"
    ```

    ```typescript Node.js theme={null}
    const res = await fetch("https://api.orbit.devotel.io/api/v1/voice/hoteling/reservations", {
      method: "POST",
      headers: { "X-API-Key": process.env.ORBIT_API_KEY!, "Content-Type": "application/json" },
      body: JSON.stringify({
        deviceSipUsername: "acme-shared-5F-2",
        startsAt: "2026-07-20T09:00:00Z",
        endsAt: "2026-07-20T17:00:00Z",
      }),
    })
    const { data } = await res.json()
    ```

    ```python Python theme={null}
    import os, requests
    headers = {"X-API-Key": os.environ["ORBIT_API_KEY"], "Content-Type": "application/json"}
    booking = requests.post(
        "https://api.orbit.devotel.io/api/v1/voice/hoteling/reservations",
        headers=headers,
        json={
            "deviceSipUsername": "acme-shared-5F-2",
            "startsAt": "2026-07-20T09:00:00Z",
            "endsAt": "2026-07-20T17:00:00Z",
        },
    ).json()["data"]
    ```
  </CodeGroup>
</RequestExample>

<ResponseExample>
  ```json 201 theme={null}
  {
    "data": {
      "id": "hdr_0123456789abcdef0123456789abcdef",
      "deviceSipUsername": "acme-shared-5F-2",
      "userId": "usr_abc123",
      "deviceLabelSnapshot": "Front Desk 5F",
      "userDisplayNameSnapshot": null,
      "startsAt": "2026-07-20T09:00:00Z",
      "endsAt": "2026-07-20T17:00:00Z",
      "status": "reserved",
      "notes": null,
      "fulfilledSessionId": null,
      "fulfilledAt": null,
      "releasedReason": null,
      "durationMinutes": 480
    }
  }
  ```
</ResponseExample>

***

## Extensions

SIP extensions for soft-phone registration. Each extension binds one user to a SIP credential pair (managed via the SIP Credentials endpoints) and can receive direct calls or appear in ring groups.

Extensions are managed end-to-end through the dashboard at **Voice → Extensions**. The underlying call-flow surface (registering, rotating credentials, observing register state) is exposed via the SIP Credentials API at `/api/v1/sip-credentials/*` (see [SIP Credentials](/api-reference/sip-credentials)).

***

## Calendars

Holiday calendars are per-organization named lists of closure dates. Attach one to an inbound route (`holiday_calendar_id`) and the route treats those dates as closed regardless of weekday business hours — calls route to your closed-hours handling (voicemail or a fallback). Each calendar carries an IANA timezone, used to decide which calendar day the current time falls on.

### Create a Calendar

`POST /api/v1/voice/calendars`

Requires the `owner` or `admin` role.

<ParamField body="name" type="string" required>
  Calendar name. Must be unique within your organization — a duplicate name returns `409`.
</ParamField>

<ParamField body="timezone" type="string">
  IANA timezone, e.g. `Europe/Istanbul`. Defaults to `UTC`.
</ParamField>

<ParamField body="dates" type="object[]">
  Closure dates. Each entry is `{ "date": "YYYY-MM-DD", "label": "..." }` where `label` is optional. Dates must be today or later (in the calendar's timezone); up to 366 entries.
</ParamField>

<RequestExample>
  <CodeGroup>
    ```bash cURL theme={null}
    curl -X POST "https://api.orbit.devotel.io/api/v1/voice/calendars" \
      -H "X-API-Key: dv_live_sk_your_key_here" \
      -H "Content-Type: application/json" \
      -d '{
      "name": "Istanbul Public Holidays",
      "timezone": "Europe/Istanbul",
      "dates": [
        { "date": "2026-04-23", "label": "National Sovereignty Day" },
        { "date": "2026-05-01", "label": "Labour Day" }
      ]
    }'
    ```

    ```typescript Node.js theme={null}
    await orbit.voice.calendars.create({
      name: 'Istanbul Public Holidays',
      timezone: 'Europe/Istanbul',
      dates: [
        { date: '2026-04-23', label: 'National Sovereignty Day' },
        { date: '2026-05-01', label: 'Labour Day' },
      ],
    })
    ```

    ```python Python theme={null}
    import os, requests
    headers = {"X-API-Key": os.environ["ORBIT_API_KEY"], "Content-Type": "application/json"}
    r = requests.post("https://api.orbit.devotel.io/api/v1/voice/calendars", headers=headers, json={
      "name": "Istanbul Public Holidays",
      "timezone": "Europe/Istanbul",
      "dates": [{"date": "2026-04-23", "label": "National Sovereignty Day"}]
    })
    print(r.json())
    ```

    ```go Go theme={null}
    package main

    import (
    	"bytes"
    	"net/http"
    	"os"
    )

    func main() {
    	body := []byte(`{
      "name": "Istanbul Public Holidays",
      "timezone": "Europe/Istanbul",
      "dates": [{"date":"2026-04-23","label":"National Sovereignty Day"}]
    }`)
    	req, _ := http.NewRequest("POST", "https://api.orbit.devotel.io/api/v1/voice/calendars", bytes.NewBuffer(body))
    	req.Header.Set("X-API-Key", os.Getenv("ORBIT_API_KEY"))
    	req.Header.Set("Content-Type", "application/json")
    	http.DefaultClient.Do(req)
    }
    ```

    ```php PHP theme={null}
    <?php
    $ch = curl_init('https://api.orbit.devotel.io/api/v1/voice/calendars');
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST');
    curl_setopt($ch, CURLOPT_HTTPHEADER, [
      'X-API-Key: ' . getenv('ORBIT_API_KEY'),
      'Content-Type: application/json',
    ]);
    curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode([
      'name' => 'Istanbul Public Holidays',
      'timezone' => 'Europe/Istanbul',
      'dates' => [['date' => '2026-04-23', 'label' => 'National Sovereignty Day']],
    ]));
    echo curl_exec($ch);
    ```
  </CodeGroup>
</RequestExample>

### List / Get / Delete

`GET /api/v1/voice/calendars`

`GET /api/v1/voice/calendars/{id}`

`DELETE /api/v1/voice/calendars/{id}`

The list response includes a `usage_count` on each calendar — how many of your inbound routes reference it. Deleting a calendar is permanent; any inbound route pointing at it has its `holiday_calendar_id` reset to null (the route keeps working, with no holiday closures). Delete returns `204`.

<RequestExample>
  <CodeGroup>
    ```bash cURL theme={null}
    curl -X GET "https://api.orbit.devotel.io/api/v1/voice/calendars" -H "X-API-Key: $K"
    curl -X GET "https://api.orbit.devotel.io/api/v1/voice/calendars/cal_abc" -H "X-API-Key: $K"
    curl -X DELETE "https://api.orbit.devotel.io/api/v1/voice/calendars/cal_abc" -H "X-API-Key: $K"
    ```

    ```typescript Node.js theme={null}
    await orbit.voice.calendars.list()
    await orbit.voice.calendars.get('cal_abc')
    await orbit.voice.calendars.delete('cal_abc')
    ```

    ```python Python theme={null}
    import os, requests
    h = {"X-API-Key": os.environ["ORBIT_API_KEY"]}
    requests.get("https://api.orbit.devotel.io/api/v1/voice/calendars", headers=h)
    requests.get("https://api.orbit.devotel.io/api/v1/voice/calendars/cal_abc", headers=h)
    requests.delete("https://api.orbit.devotel.io/api/v1/voice/calendars/cal_abc", headers=h)
    ```

    ```go Go theme={null}
    package main

    import (
    	"net/http"
    	"os"
    )

    func main() {
    	req, _ := http.NewRequest("GET", "https://api.orbit.devotel.io/api/v1/voice/calendars", nil)
    	req.Header.Set("X-API-Key", os.Getenv("ORBIT_API_KEY"))
    	http.DefaultClient.Do(req)
    }
    ```

    ```php PHP theme={null}
    <?php
    $ch = curl_init('https://api.orbit.devotel.io/api/v1/voice/calendars');
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($ch, CURLOPT_HTTPHEADER, ['X-API-Key: ' . getenv('ORBIT_API_KEY')]);
    echo curl_exec($ch);
    ```
  </CodeGroup>
</RequestExample>

### Update a Calendar

`PATCH /api/v1/voice/calendars/{id}`

Requires the `owner` or `admin` role. Send any subset of `name`, `timezone`, or `dates`; omitted fields are left unchanged. When you send `dates`, it fully replaces the existing list rather than merging.

<RequestExample>
  <CodeGroup>
    ```bash cURL theme={null}
    curl -X PATCH "https://api.orbit.devotel.io/api/v1/voice/calendars/cal_abc" \
      -H "X-API-Key: dv_live_sk_your_key_here" \
      -H "Content-Type: application/json" \
      -d '{ "dates": [{ "date": "2026-12-25", "label": "Christmas Day" }] }'
    ```

    ```typescript Node.js theme={null}
    await orbit.voice.calendars.update('cal_abc', {
      dates: [{ date: '2026-12-25', label: 'Christmas Day' }],
    })
    ```

    ```python Python theme={null}
    import os, requests
    headers = {"X-API-Key": os.environ["ORBIT_API_KEY"], "Content-Type": "application/json"}
    requests.patch("https://api.orbit.devotel.io/api/v1/voice/calendars/cal_abc", headers=headers, json={
      "dates": [{"date": "2026-12-25", "label": "Christmas Day"}]
    })
    ```
  </CodeGroup>
</RequestExample>

***

## Monitoring

Live supervisor surface — listen-in, whisper to the agent, or barge into a call for real-time coaching. All operations require the `voice:monitor` scope.

### Start Listening (silent monitoring)

`POST /api/v1/voice/calls/{id}/listen`

Open a one-way audio bridge to the supervisor — neither call leg hears the supervisor.

`POST /api/v1/voice/calls/{id}/unlisten`

Disconnect the supervisor leg.

<RequestExample>
  <CodeGroup>
    ```bash cURL theme={null}
    curl -X POST "https://api.orbit.devotel.io/api/v1/voice/calls/call_abc123/listen" \
      -H "X-API-Key: dv_live_sk_your_key_here"
    ```

    ```typescript Node.js theme={null}
    await orbit.voice.supervisor.listen('call_abc123')
    await orbit.voice.supervisor.unlisten('call_abc123')
    ```

    ```python Python theme={null}
    import os, requests
    headers = {"X-API-Key": os.environ["ORBIT_API_KEY"]}
    r = requests.post("https://api.orbit.devotel.io/api/v1/voice/calls/call_abc123/listen", headers=headers)
    print(r.json())
    ```

    ```go Go theme={null}
    package main

    import (
    	"net/http"
    	"os"
    )

    func main() {
    	req, _ := http.NewRequest("POST",
    		"https://api.orbit.devotel.io/api/v1/voice/calls/call_abc123/listen", nil)
    	req.Header.Set("X-API-Key", os.Getenv("ORBIT_API_KEY"))
    	http.DefaultClient.Do(req)
    }
    ```

    ```php PHP theme={null}
    <?php
    $ch = curl_init('https://api.orbit.devotel.io/api/v1/voice/calls/call_abc123/listen');
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST');
    curl_setopt($ch, CURLOPT_HTTPHEADER, ['X-API-Key: ' . getenv('ORBIT_API_KEY')]);
    echo curl_exec($ch);
    ```
  </CodeGroup>
</RequestExample>

### Whisper to the Agent

`POST /api/v1/voice/calls/{id}/whisper`

Audio plays only to the agent leg. The customer cannot hear. Use to coach the agent mid-call.

<RequestExample>
  <CodeGroup>
    ```bash cURL theme={null}
    curl -X POST "https://api.orbit.devotel.io/api/v1/voice/calls/call_abc123/whisper" \
      -H "X-API-Key: dv_live_sk_your_key_here"
    ```

    ```typescript Node.js theme={null}
    await orbit.voice.supervisor.whisper('call_abc123')
    ```

    ```python Python theme={null}
    import os, requests
    headers = {"X-API-Key": os.environ["ORBIT_API_KEY"]}
    r = requests.post("https://api.orbit.devotel.io/api/v1/voice/calls/call_abc123/whisper", headers=headers)
    print(r.json())
    ```

    ```go Go theme={null}
    package main

    import (
    	"net/http"
    	"os"
    )

    func main() {
    	req, _ := http.NewRequest("POST",
    		"https://api.orbit.devotel.io/api/v1/voice/calls/call_abc123/whisper", nil)
    	req.Header.Set("X-API-Key", os.Getenv("ORBIT_API_KEY"))
    	http.DefaultClient.Do(req)
    }
    ```

    ```php PHP theme={null}
    <?php
    $ch = curl_init('https://api.orbit.devotel.io/api/v1/voice/calls/call_abc123/whisper');
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST');
    curl_setopt($ch, CURLOPT_HTTPHEADER, ['X-API-Key: ' . getenv('ORBIT_API_KEY')]);
    echo curl_exec($ch);
    ```
  </CodeGroup>
</RequestExample>

### Barge In

`POST /api/v1/voice/calls/{id}/barge`

Add the supervisor as a third audible participant. Both call legs hear the supervisor.

<RequestExample>
  <CodeGroup>
    ```bash cURL theme={null}
    curl -X POST "https://api.orbit.devotel.io/api/v1/voice/calls/call_abc123/barge" \
      -H "X-API-Key: dv_live_sk_your_key_here"
    ```

    ```typescript Node.js theme={null}
    await orbit.voice.supervisor.barge('call_abc123')
    ```

    ```python Python theme={null}
    import os, requests
    headers = {"X-API-Key": os.environ["ORBIT_API_KEY"]}
    r = requests.post("https://api.orbit.devotel.io/api/v1/voice/calls/call_abc123/barge", headers=headers)
    print(r.json())
    ```

    ```go Go theme={null}
    package main

    import (
    	"net/http"
    	"os"
    )

    func main() {
    	req, _ := http.NewRequest("POST",
    		"https://api.orbit.devotel.io/api/v1/voice/calls/call_abc123/barge", nil)
    	req.Header.Set("X-API-Key", os.Getenv("ORBIT_API_KEY"))
    	http.DefaultClient.Do(req)
    }
    ```

    ```php PHP theme={null}
    <?php
    $ch = curl_init('https://api.orbit.devotel.io/api/v1/voice/calls/call_abc123/barge');
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST');
    curl_setopt($ch, CURLOPT_HTTPHEADER, ['X-API-Key: ' . getenv('ORBIT_API_KEY')]);
    echo curl_exec($ch);
    ```
  </CodeGroup>
</RequestExample>

### Live Monitoring Snapshot

`GET /api/v1/voice/monitoring`

Returns a denormalised snapshot of every active call, queue, and agent presence — drives the wallboard / Voice → Monitoring page.

<RequestExample>
  <CodeGroup>
    ```bash cURL theme={null}
    curl -X GET "https://api.orbit.devotel.io/api/v1/voice/monitoring" \
      -H "X-API-Key: dv_live_sk_your_key_here"
    ```

    ```typescript Node.js theme={null}
    const snap = await orbit.voice.monitoring.snapshot()
    console.log(snap.data.active_calls)
    ```

    ```python Python theme={null}
    import os, requests
    headers = {"X-API-Key": os.environ["ORBIT_API_KEY"]}
    r = requests.get("https://api.orbit.devotel.io/api/v1/voice/monitoring", headers=headers)
    print(r.json())
    ```

    ```go Go theme={null}
    package main

    import (
    	"net/http"
    	"os"
    )

    func main() {
    	req, _ := http.NewRequest("GET", "https://api.orbit.devotel.io/api/v1/voice/monitoring", nil)
    	req.Header.Set("X-API-Key", os.Getenv("ORBIT_API_KEY"))
    	http.DefaultClient.Do(req)
    }
    ```

    ```php PHP theme={null}
    <?php
    $ch = curl_init('https://api.orbit.devotel.io/api/v1/voice/monitoring');
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($ch, CURLOPT_HTTPHEADER, ['X-API-Key: ' . getenv('ORBIT_API_KEY')]);
    echo curl_exec($ch);
    ```
  </CodeGroup>
</RequestExample>

<ResponseExample>
  ```json 200 theme={null}
  {
    "data": {
      "active_calls": 14,
      "queued_calls": 3,
      "available_agents": 7,
      "busy_agents": 5,
      "calls": [
        {
          "id": "call_abc123",
          "from": "+14155552671",
          "to": "+18005551234",
          "agent_id": "agt_support_jane",
          "duration_seconds": 87,
          "queue_id": "queue_support",
          "started_at": "2026-03-08T11:58:33Z"
        }
      ]
    }
  }
  ```
</ResponseExample>

***

## Real-time Transcripts

`GET /api/v1/voice/calls/{callId}/transcript/stream`

Server-Sent Events (SSE) stream of live STT transcript chunks for an in-progress call. Each event carries a JSON body of:

```
{ "speaker": "agent" | "caller", "text": "...", "ts": "ISO-8601" }
```

Use for live captions, real-time agent assist, or downstream sentiment / intent classification. The stream closes when the call ends.

`GET /api/v1/voice/calls/{id}/transcript`

Final, post-call transcript (one shot, no streaming). Available once the recording has been processed (\~30s after hangup).

`POST /api/v1/voice/transcribe`

Submit an arbitrary audio URL for asynchronous transcription. Returns a `transcript_id` to poll for the result.

<RequestExample>
  <CodeGroup>
    ```bash cURL theme={null}
    # SSE stream
    curl -N -X GET "https://api.orbit.devotel.io/api/v1/voice/calls/call_abc123/transcript/stream" \
      -H "X-API-Key: dv_live_sk_your_key_here" \
      -H "Accept: text/event-stream"
    # Final post-call transcript
    curl -X GET "https://api.orbit.devotel.io/api/v1/voice/calls/call_abc123/transcript" \
      -H "X-API-Key: dv_live_sk_your_key_here"
    # Submit audio URL for async transcription
    curl -X POST "https://api.orbit.devotel.io/api/v1/voice/transcribe" \
      -H "X-API-Key: dv_live_sk_your_key_here" \
      -H "Content-Type: application/json" \
      -d '{ "audio_url": "https://example.com/audio.mp3", "language": "en-US" }'
    ```

    ```typescript Node.js theme={null}
    // Final transcript
    const tx = await orbit.voice.transcripts.get('call_abc123')
    console.log(tx.data.utterances)
    // Async submit
    const job = await orbit.voice.transcripts.submit({
      audio_url: 'https://example.com/audio.mp3',
      language: 'en-US',
    })
    console.log(job.data.transcript_id)
    ```

    ```python Python theme={null}
    import os, requests
    h = {"X-API-Key": os.environ["ORBIT_API_KEY"]}
    requests.get("https://api.orbit.devotel.io/api/v1/voice/calls/call_abc123/transcript", headers=h)
    requests.post("https://api.orbit.devotel.io/api/v1/voice/transcribe",
                  headers={**h, "Content-Type": "application/json"},
                  json={"audio_url": "https://example.com/audio.mp3", "language": "en-US"})
    ```

    ```go Go theme={null}
    package main

    import (
    	"bytes"
    	"net/http"
    	"os"
    )

    func main() {
    	req, _ := http.NewRequest("POST", "https://api.orbit.devotel.io/api/v1/voice/transcribe",
    		bytes.NewBuffer([]byte(`{"audio_url":"https://example.com/audio.mp3","language":"en-US"}`)))
    	req.Header.Set("X-API-Key", os.Getenv("ORBIT_API_KEY"))
    	req.Header.Set("Content-Type", "application/json")
    	http.DefaultClient.Do(req)
    }
    ```

    ```php PHP theme={null}
    <?php
    $ch = curl_init('https://api.orbit.devotel.io/api/v1/voice/transcribe');
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST');
    curl_setopt($ch, CURLOPT_HTTPHEADER, [
      'X-API-Key: ' . getenv('ORBIT_API_KEY'),
      'Content-Type: application/json',
    ]);
    curl_setopt($ch, CURLOPT_POSTFIELDS, '{"audio_url":"https://example.com/audio.mp3","language":"en-US"}');
    echo curl_exec($ch);
    ```
  </CodeGroup>
</RequestExample>

***

## AI Call Intelligence

Post-call analysis — sentiment trajectory, key moments, summary, action items, customer effort score. Powered by the agent runtime's call intelligence model.

### Per-Call Intelligence

`GET /api/v1/voice/calls/{id}/intelligence`

<RequestExample>
  <CodeGroup>
    ```bash cURL theme={null}
    curl -X GET "https://api.orbit.devotel.io/api/v1/voice/calls/call_abc123/intelligence" \
      -H "X-API-Key: dv_live_sk_your_key_here"
    ```

    ```typescript Node.js theme={null}
    const intel = await orbit.voice.intelligence.getForCall('call_abc123')
    console.log(intel.data.summary)
    ```

    ```python Python theme={null}
    import os, requests
    headers = {"X-API-Key": os.environ["ORBIT_API_KEY"]}
    r = requests.get("https://api.orbit.devotel.io/api/v1/voice/calls/call_abc123/intelligence", headers=headers)
    print(r.json())
    ```

    ```go Go theme={null}
    package main

    import (
    	"net/http"
    	"os"
    )

    func main() {
    	req, _ := http.NewRequest("GET",
    		"https://api.orbit.devotel.io/api/v1/voice/calls/call_abc123/intelligence", nil)
    	req.Header.Set("X-API-Key", os.Getenv("ORBIT_API_KEY"))
    	http.DefaultClient.Do(req)
    }
    ```

    ```php PHP theme={null}
    <?php
    $ch = curl_init('https://api.orbit.devotel.io/api/v1/voice/calls/call_abc123/intelligence');
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($ch, CURLOPT_HTTPHEADER, ['X-API-Key: ' . getenv('ORBIT_API_KEY')]);
    echo curl_exec($ch);
    ```
  </CodeGroup>
</RequestExample>

<ResponseExample>
  ```json 200 theme={null}
  {
    "data": {
      "call_id": "call_abc123",
      "summary": "Customer called to dispute a duplicate charge dated 2026-03-05 for $89.00. Agent confirmed the charge was a billing system retry artifact, refunded immediately, sent confirmation email.",
      "sentiment": {
        "overall": "neutral_to_positive",
        "trajectory": "improved",
        "agent_score": 0.82,
        "caller_score": 0.65
      },
      "key_moments": [
        { "ts": 12, "type": "intent_detected", "label": "billing_dispute" },
        { "ts": 47, "type": "frustration_peak", "score": 0.78 },
        { "ts": 134, "type": "resolution_offered" },
        { "ts": 189, "type": "satisfaction_signal", "score": 0.85 }
      ],
      "action_items": [
        "Send refund confirmation email by EOD",
        "Flag billing system for duplicate-charge investigation"
      ],
      "customer_effort_score": 2.4,
      "agent_compliance_flags": [],
      "language": "en-US",
      "processed_at": "2026-03-08T12:08:14Z"
    }
  }
  ```
</ResponseExample>

### Run Sentiment On-Demand

`POST /api/v1/voice/calls/{id}/sentiment`

Forces a re-run of the sentiment analyzer. Used when a call's transcript has been edited or when the org's sentiment model has changed.

<RequestExample>
  <CodeGroup>
    ```bash cURL theme={null}
    curl -X POST "https://api.orbit.devotel.io/api/v1/voice/calls/call_abc123/sentiment" \
      -H "X-API-Key: dv_live_sk_your_key_here"
    ```

    ```typescript Node.js theme={null}
    await orbit.voice.intelligence.runSentiment('call_abc123')
    ```

    ```python Python theme={null}
    import os, requests
    headers = {"X-API-Key": os.environ["ORBIT_API_KEY"]}
    r = requests.post("https://api.orbit.devotel.io/api/v1/voice/calls/call_abc123/sentiment", headers=headers)
    print(r.json())
    ```

    ```go Go theme={null}
    package main

    import (
    	"net/http"
    	"os"
    )

    func main() {
    	req, _ := http.NewRequest("POST",
    		"https://api.orbit.devotel.io/api/v1/voice/calls/call_abc123/sentiment", nil)
    	req.Header.Set("X-API-Key", os.Getenv("ORBIT_API_KEY"))
    	http.DefaultClient.Do(req)
    }
    ```

    ```php PHP theme={null}
    <?php
    $ch = curl_init('https://api.orbit.devotel.io/api/v1/voice/calls/call_abc123/sentiment');
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST');
    curl_setopt($ch, CURLOPT_HTTPHEADER, ['X-API-Key: ' . getenv('ORBIT_API_KEY')]);
    echo curl_exec($ch);
    ```
  </CodeGroup>
</RequestExample>

### List Intelligence Records

`GET /api/v1/voice/intelligence/calls`

List intelligence records for the org with filters by sentiment, agent, date range, and customer-effort threshold.

<RequestExample>
  <CodeGroup>
    ```bash cURL theme={null}
    curl -X GET "https://api.orbit.devotel.io/api/v1/voice/intelligence/calls?sentiment=negative&limit=50" \
      -H "X-API-Key: dv_live_sk_your_key_here"
    ```

    ```typescript Node.js theme={null}
    await orbit.voice.intelligence.listCalls({ sentiment: 'negative', limit: 50 })
    ```

    ```python Python theme={null}
    import os, requests
    headers = {"X-API-Key": os.environ["ORBIT_API_KEY"]}
    r = requests.get("https://api.orbit.devotel.io/api/v1/voice/intelligence/calls",
                     headers=headers, params={"sentiment": "negative", "limit": 50})
    print(r.json())
    ```

    ```go Go theme={null}
    package main

    import (
    	"net/http"
    	"os"
    )

    func main() {
    	req, _ := http.NewRequest("GET",
    		"https://api.orbit.devotel.io/api/v1/voice/intelligence/calls?sentiment=negative&limit=50", nil)
    	req.Header.Set("X-API-Key", os.Getenv("ORBIT_API_KEY"))
    	http.DefaultClient.Do(req)
    }
    ```

    ```php PHP theme={null}
    <?php
    $ch = curl_init('https://api.orbit.devotel.io/api/v1/voice/intelligence/calls?sentiment=negative&limit=50');
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($ch, CURLOPT_HTTPHEADER, ['X-API-Key: ' . getenv('ORBIT_API_KEY')]);
    echo curl_exec($ch);
    ```
  </CodeGroup>
</RequestExample>

### Trends

`GET /api/v1/voice/intelligence/trends`

Aggregated time-series of sentiment + effort score, bucketed by day / week. Drives the **Voice → Intelligence → Trends** dashboard.

<ParamField query="from" type="string">
  ISO-8601 start of the trend window. Defaults to 30 days ago.
</ParamField>

<ParamField query="to" type="string">
  ISO-8601 end of the trend window. Defaults to now.
</ParamField>

<ParamField query="bucket" type="string" default="day">
  Aggregation bucket: `hour`, `day`, `week`.
</ParamField>

<RequestExample>
  <CodeGroup>
    ```bash cURL theme={null}
    curl -X GET "https://api.orbit.devotel.io/api/v1/voice/intelligence/trends?from=2026-02-01&to=2026-03-08&bucket=day" \
      -H "X-API-Key: dv_live_sk_your_key_here"
    ```

    ```typescript Node.js theme={null}
    await orbit.voice.intelligence.trends({
      from: '2026-02-01',
      to: '2026-03-08',
      bucket: 'day',
    })
    ```

    ```python Python theme={null}
    import os, requests
    headers = {"X-API-Key": os.environ["ORBIT_API_KEY"]}
    r = requests.get("https://api.orbit.devotel.io/api/v1/voice/intelligence/trends", headers=headers,
                     params={"from": "2026-02-01", "to": "2026-03-08", "bucket": "day"})
    print(r.json())
    ```

    ```go Go theme={null}
    package main

    import (
    	"net/http"
    	"os"
    )

    func main() {
    	req, _ := http.NewRequest("GET",
    		"https://api.orbit.devotel.io/api/v1/voice/intelligence/trends?from=2026-02-01&to=2026-03-08&bucket=day", nil)
    	req.Header.Set("X-API-Key", os.Getenv("ORBIT_API_KEY"))
    	http.DefaultClient.Do(req)
    }
    ```

    ```php PHP theme={null}
    <?php
    $ch = curl_init('https://api.orbit.devotel.io/api/v1/voice/intelligence/trends?from=2026-02-01&to=2026-03-08&bucket=day');
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($ch, CURLOPT_HTTPHEADER, ['X-API-Key: ' . getenv('ORBIT_API_KEY')]);
    echo curl_exec($ch);
    ```
  </CodeGroup>
</RequestExample>

***

## Voice Quality (VAQI)

Voice Aggregated Quality Index — a 0–100 score derived from MOS, jitter, packet-loss, and one-way audio detection. Surface for SIP-trunk health and per-call diagnostics.

### Aggregates

`GET /api/v1/voice/quality/aggregates`

Per-period rollup of every active SIP trunk: average MOS, p95 jitter, packet loss percentage, VAQI score. Used by the Voice → Quality dashboard.

### Per-Carrier Quality

`GET /api/v1/voice/quality/carriers`

Same shape as `/aggregates` but bucketed by upstream carrier. Use to compare provider performance across the same time window.

### Worst Calls

`GET /api/v1/voice/quality/worst-calls`

Top-N calls in the period by lowest VAQI / highest packet loss / highest jitter. Drives the "Investigate" CTA in the dashboard.

### VAQI Detail

`GET /api/v1/voice/vaqi`

Full per-call quality matrix (MOS, jitter, packet loss, RTT, audio gaps) with optional `call_id` filter for a single-call drill-down.

<RequestExample>
  <CodeGroup>
    ```bash cURL theme={null}
    curl -X GET "https://api.orbit.devotel.io/api/v1/voice/quality/aggregates" -H "X-API-Key: $K"
    curl -X GET "https://api.orbit.devotel.io/api/v1/voice/quality/carriers" -H "X-API-Key: $K"
    curl -X GET "https://api.orbit.devotel.io/api/v1/voice/quality/worst-calls?limit=10" -H "X-API-Key: $K"
    curl -X GET "https://api.orbit.devotel.io/api/v1/voice/vaqi?call_id=call_abc123" -H "X-API-Key: $K"
    ```

    ```typescript Node.js theme={null}
    await orbit.voice.quality.aggregates()
    await orbit.voice.quality.carriers()
    await orbit.voice.quality.worstCalls({ limit: 10 })
    await orbit.voice.quality.vaqi({ call_id: 'call_abc123' })
    ```

    ```python Python theme={null}
    import os, requests
    h = {"X-API-Key": os.environ["ORBIT_API_KEY"]}
    requests.get("https://api.orbit.devotel.io/api/v1/voice/quality/aggregates", headers=h)
    requests.get("https://api.orbit.devotel.io/api/v1/voice/quality/carriers", headers=h)
    requests.get("https://api.orbit.devotel.io/api/v1/voice/quality/worst-calls", headers=h, params={"limit": 10})
    requests.get("https://api.orbit.devotel.io/api/v1/voice/vaqi", headers=h, params={"call_id": "call_abc123"})
    ```

    ```go Go theme={null}
    package main

    import (
    	"net/http"
    	"os"
    )

    func main() {
    	req, _ := http.NewRequest("GET", "https://api.orbit.devotel.io/api/v1/voice/quality/aggregates", nil)
    	req.Header.Set("X-API-Key", os.Getenv("ORBIT_API_KEY"))
    	http.DefaultClient.Do(req)
    }
    ```

    ```php PHP theme={null}
    <?php
    $ch = curl_init('https://api.orbit.devotel.io/api/v1/voice/quality/aggregates');
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($ch, CURLOPT_HTTPHEADER, ['X-API-Key: ' . getenv('ORBIT_API_KEY')]);
    echo curl_exec($ch);
    ```
  </CodeGroup>
</RequestExample>

***

## Softphone Token

`POST /api/v1/voice/softphone/token`

Mints a short-lived JWT (default 60s expiry) for browser-based softphone clients to register against the Orbit Media / Jambonz cluster without exposing a long-lived API key. Orbit Media is forked from LiveKit OSS under Apache-2 — see [attribution](/legal/attribution).

`POST /api/v1/voice/softphone/dial`

Server-initiated outbound dial from a softphone session — the softphone client passes the `to` and Orbit places the call from the bound extension's caller-ID.

<RequestExample>
  <CodeGroup>
    ```bash cURL theme={null}
    # Mint a JWT
    curl -X POST "https://api.orbit.devotel.io/api/v1/voice/softphone/token" \
      -H "X-API-Key: dv_live_sk_your_key_here" \
      -H "Content-Type: application/json" \
      -d '{ "extension_id": "ext_alice", "ttl_seconds": 60 }'
    # Server-initiated outbound dial
    curl -X POST "https://api.orbit.devotel.io/api/v1/voice/softphone/dial" \
      -H "X-API-Key: dv_live_sk_your_key_here" \
      -H "Content-Type: application/json" \
      -d '{ "to": "+14155557890", "extension_id": "ext_alice" }'
    ```

    ```typescript Node.js theme={null}
    const jwt = await orbit.voice.softphone.mintToken({
      extension_id: 'ext_alice',
      ttl_seconds: 60,
    })
    await orbit.voice.softphone.dial({ to: '+14155557890', extension_id: 'ext_alice' })
    ```

    ```python Python theme={null}
    import os, requests
    headers = {"X-API-Key": os.environ["ORBIT_API_KEY"], "Content-Type": "application/json"}
    r = requests.post("https://api.orbit.devotel.io/api/v1/voice/softphone/token", headers=headers,
                      json={"extension_id": "ext_alice", "ttl_seconds": 60})
    print(r.json())
    ```

    ```go Go theme={null}
    package main

    import (
    	"bytes"
    	"net/http"
    	"os"
    )

    func main() {
    	req, _ := http.NewRequest("POST",
    		"https://api.orbit.devotel.io/api/v1/voice/softphone/token",
    		bytes.NewBuffer([]byte(`{"extension_id":"ext_alice","ttl_seconds":60}`)))
    	req.Header.Set("X-API-Key", os.Getenv("ORBIT_API_KEY"))
    	req.Header.Set("Content-Type", "application/json")
    	http.DefaultClient.Do(req)
    }
    ```

    ```php PHP theme={null}
    <?php
    $ch = curl_init('https://api.orbit.devotel.io/api/v1/voice/softphone/token');
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST');
    curl_setopt($ch, CURLOPT_HTTPHEADER, [
      'X-API-Key: ' . getenv('ORBIT_API_KEY'),
      'Content-Type: application/json',
    ]);
    curl_setopt($ch, CURLOPT_POSTFIELDS, '{"extension_id":"ext_alice","ttl_seconds":60}');
    echo curl_exec($ch);
    ```
  </CodeGroup>
</RequestExample>

***

## Voice Clones

Custom-trained TTS voices for AI agents. Voices are tenant-scoped. A clone is sourced from an existing recorded call on your account — you do not upload an audio sample. The source call must have a granted, active recording-consent receipt, and you must attest to the voice owner's permission when you create the clone.

### Create

`POST /api/v1/voice/clones`

Request body:

| Field                              | Type    | Required    | Description                                                                                                                                            |
| ---------------------------------- | ------- | ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `name`                             | string  | yes         | Display name for the voice (1–80 chars).                                                                                                               |
| `call_id`                          | string  | yes         | ID of an existing recorded call to clone the voice from. The call must carry a granted, non-revoked recording-consent receipt.                         |
| `voice_owner_relationship`         | string  | yes         | Your relationship to the person being cloned. One of `self`, `employee`, `actor_with_release`, `customer_with_written_consent`, `other`.               |
| `biometric_consent_acknowledged`   | boolean | yes         | Must be `true`. Confirms you understand that a voiceprint is biometric data governed by laws such as BIPA (Illinois), CUBI (Texas), and the EU AI Act. |
| `voice_owner_consent_evidence_url` | string  | conditional | HTTPS URL to your stored proof of the voice owner's consent (for example a signed PDF, a DocuSign envelope, or an in-app consent screenshot).          |
| `voice_owner_consent_text`         | string  | conditional | Free-form attestation (≥ 50 chars) describing how consent was obtained.                                                                                |

You must supply at least one of `voice_owner_consent_evidence_url` or `voice_owner_consent_text`. A request that clones a call without a granted recording-consent receipt, or without a consent attestation, is rejected with `400`.

The response returns the clone `id`, its `status` (`processing`, `ready`, or `failed`), the `source_call_id`, and `created_at`. Cloning runs asynchronously; poll `GET /api/v1/voice/clones` until the status is `ready`.

### List / Delete

`GET /api/v1/voice/clones`

`DELETE /api/v1/voice/clones/{id}`

Voice IDs returned by `GET /api/v1/voice/clones` can be referenced from agent configuration via `tts_voice_id`.

<RequestExample>
  <CodeGroup>
    ```bash cURL theme={null}
    # Create — clone from an existing recorded call
    curl -X POST "https://api.orbit.devotel.io/api/v1/voice/clones" \
      -H "X-API-Key: dv_live_sk_your_key_here" \
      -H "Content-Type: application/json" \
      -d '{
      "name": "Brand Voice — EN",
      "call_id": "call_abc123",
      "voice_owner_relationship": "employee",
      "biometric_consent_acknowledged": true,
      "voice_owner_consent_evidence_url": "https://storage.example.com/consent/alice.pdf",
      "voice_owner_consent_text": "Alice signed our recorded voice-cloning release on 2026-06-30; PDF stored at the evidence URL."
    }'
    # List
    curl -X GET "https://api.orbit.devotel.io/api/v1/voice/clones" -H "X-API-Key: $K"
    # Delete
    curl -X DELETE "https://api.orbit.devotel.io/api/v1/voice/clones/clone_abc" -H "X-API-Key: $K"
    ```

    ```typescript Node.js theme={null}
    await orbit.voice.clones.create({
      name: 'Brand Voice — EN',
      call_id: 'call_abc123',
      voice_owner_relationship: 'employee',
      biometric_consent_acknowledged: true,
      voice_owner_consent_evidence_url: 'https://storage.example.com/consent/alice.pdf',
      voice_owner_consent_text:
        'Alice signed our recorded voice-cloning release on 2026-06-30; PDF stored at the evidence URL.',
    })
    await orbit.voice.clones.list()
    await orbit.voice.clones.delete('clone_abc')
    ```

    ```python Python theme={null}
    import os, requests
    h = {"X-API-Key": os.environ["ORBIT_API_KEY"], "Content-Type": "application/json"}
    r = requests.post("https://api.orbit.devotel.io/api/v1/voice/clones", headers=h, json={
      "name": "Brand Voice — EN",
      "call_id": "call_abc123",
      "voice_owner_relationship": "employee",
      "biometric_consent_acknowledged": True,
      "voice_owner_consent_evidence_url": "https://storage.example.com/consent/alice.pdf",
      "voice_owner_consent_text": "Alice signed our recorded voice-cloning release on 2026-06-30; PDF stored at the evidence URL."
    })
    print(r.json())
    ```

    ```go Go theme={null}
    package main

    import (
    	"bytes"
    	"net/http"
    	"os"
    )

    func main() {
    	body := []byte(`{
      "name": "Brand Voice — EN",
      "call_id": "call_abc123",
      "voice_owner_relationship": "employee",
      "biometric_consent_acknowledged": true,
      "voice_owner_consent_evidence_url": "https://storage.example.com/consent/alice.pdf",
      "voice_owner_consent_text": "Alice signed our recorded voice-cloning release on 2026-06-30; PDF stored at the evidence URL."
    }`)
    	req, _ := http.NewRequest("POST", "https://api.orbit.devotel.io/api/v1/voice/clones", bytes.NewBuffer(body))
    	req.Header.Set("X-API-Key", os.Getenv("ORBIT_API_KEY"))
    	req.Header.Set("Content-Type", "application/json")
    	http.DefaultClient.Do(req)
    }
    ```

    ```php PHP theme={null}
    <?php
    $ch = curl_init('https://api.orbit.devotel.io/api/v1/voice/clones');
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST');
    curl_setopt($ch, CURLOPT_HTTPHEADER, [
      'X-API-Key: ' . getenv('ORBIT_API_KEY'),
      'Content-Type: application/json',
    ]);
    curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode([
      'name' => 'Brand Voice — EN',
      'call_id' => 'call_abc123',
      'voice_owner_relationship' => 'employee',
      'biometric_consent_acknowledged' => true,
      'voice_owner_consent_evidence_url' => 'https://storage.example.com/consent/alice.pdf',
      'voice_owner_consent_text' => 'Alice signed our recorded voice-cloning release on 2026-06-30; PDF stored at the evidence URL.',
    ]));
    echo curl_exec($ch);
    ```
  </CodeGroup>
</RequestExample>

***

## Coaching Notes

Supervisors attach a structured, acknowledgeable coaching note to a specific call, targeting the agent who handled it. Notes appear on the agent's coaching timeline; the agent acknowledges each one. Writing a note requires an owner, admin, or supervisor role.

### Create a Coaching Note

`POST /api/v1/voice/calls/{id}/coaching-notes`

<ParamField path="id" type="string" required>
  The call the note is anchored to.
</ParamField>

<ParamField body="agent_user_id" type="string" required>
  The agent the note coaches. Must be the agent who handled this call — a mismatch is rejected with `422 AGENT_MISMATCH`.
</ParamField>

<ParamField body="body" type="string" required>
  The coaching text (1–4000 characters).
</ParamField>

<ParamField body="severity" type="string" default="general">
  Note category: `positive` (commendation), `improvement` (coachable moment), `warning` (policy or performance flag), or `general` (informational).
</ParamField>

<ParamField body="rubric_ids" type="string[]">
  Optional QA-rubric references this note maps to (up to 10).
</ParamField>

<ParamField body="suggested_action" type="string">
  Optional follow-up action for the agent (up to 500 characters).
</ParamField>

### List Coaching Notes on a Call

`GET /api/v1/voice/calls/{id}/coaching-notes`

Owners, admins, and supervisors see every note on the call. An agent sees only the notes addressed to them.

### Acknowledge a Coaching Note

`PUT /api/v1/voice/calls/{id}/coaching-notes/{noteId}/acknowledge`

Only the agent the note is addressed to may acknowledge it. Acknowledgement is idempotent — a repeat call returns the original acknowledgement timestamp.

<RequestExample>
  <CodeGroup>
    ```bash cURL theme={null}
    # Author a note
    curl -X POST "https://api.orbit.devotel.io/api/v1/voice/calls/call_abc123/coaching-notes" \
      -H "X-API-Key: dv_live_sk_your_key_here" \
      -H "Content-Type: application/json" \
      -d '{
      "agent_user_id": "usr_agent_42",
      "body": "Strong discovery questions. Confirm the account number before disclosing balances next time.",
      "severity": "improvement",
      "suggested_action": "Review the identity-verification script section 2."
    }'
    # Agent acknowledges
    curl -X PUT "https://api.orbit.devotel.io/api/v1/voice/calls/call_abc123/coaching-notes/cnote_9f2/acknowledge" \
      -H "X-API-Key: dv_live_sk_your_key_here"
    ```

    ```typescript Node.js theme={null}
    await orbit.voice.coachingNotes.create('call_abc123', {
      agent_user_id: 'usr_agent_42',
      body: 'Strong discovery questions. Confirm the account number before disclosing balances next time.',
      severity: 'improvement',
      suggested_action: 'Review the identity-verification script section 2.',
    })

    await orbit.voice.coachingNotes.acknowledge('call_abc123', 'cnote_9f2')
    ```

    ```python Python theme={null}
    import os, requests
    headers = {"X-API-Key": os.environ["ORBIT_API_KEY"], "Content-Type": "application/json"}
    requests.post("https://api.orbit.devotel.io/api/v1/voice/calls/call_abc123/coaching-notes",
                  headers=headers, json={
      "agent_user_id": "usr_agent_42",
      "body": "Strong discovery questions. Confirm the account number before disclosing balances next time.",
      "severity": "improvement"
    })
    ```
  </CodeGroup>
</RequestExample>

***

## Coaching Plans

A coaching plan wraps a longitudinal, multi-session improvement program for one agent — a goal, an optional due date, and a lifecycle that closes with an outcome. Writes require an owner, admin, or supervisor role; an agent can read only their own plans.

### Create a Coaching Plan

`POST /api/v1/voice/coaching-plans`

<ParamField body="agent_user_id" type="string" required>
  The agent the plan addresses.
</ParamField>

<ParamField body="goal" type="string" required>
  The goal statement (1–2000 characters), e.g. "Reduce average handle time by 20% over four weeks."
</ParamField>

<ParamField body="due_at" type="string">
  Optional target completion timestamp (ISO 8601). Omit for an open-ended plan the supervisor closes manually.
</ParamField>

### List Coaching Plans

`GET /api/v1/voice/coaching-plans`

<ParamField query="agent_user_id" type="string">
  Filter to one agent's plans.
</ParamField>

<ParamField query="owner_user_id" type="string">
  Filter to plans authored by one supervisor.
</ParamField>

<ParamField query="status" type="string">
  Filter by lifecycle status: `active`, `completed`, or `cancelled`.
</ParamField>

<ParamField query="limit" type="integer" default="50">
  Results to return (1–200). Active plans sort first, then by due date.
</ParamField>

### Get a Coaching Plan

`GET /api/v1/voice/coaching-plans/{id}`

### Update a Coaching Plan

`PUT /api/v1/voice/coaching-plans/{id}`

Only `active` plans can be updated — updating a closed plan returns `409 PLAN_CLOSED`. Supply at least one of `agent_user_id`, `goal`, or `due_at`.

### Close a Coaching Plan

`POST /api/v1/voice/coaching-plans/{id}/close`

<ParamField body="status" type="string" default="completed">
  Terminal status: `completed` or `cancelled`.
</ParamField>

<ParamField body="outcome" type="string" required>
  Supervisor judgement of the result: `met`, `partial`, or `missed`.
</ParamField>

<ParamField body="outcome_notes" type="string">
  Optional narrative on what changed (up to 2000 characters).
</ParamField>

### Coaching-Plan Effectiveness

`GET /api/v1/voice/coaching-plans/{id}/effectiveness`

Returns a data-driven before/after report: the agent's Average Handle Time, First-Contact Resolution, and CSAT measured over a window before the plan compared against an equal window after it, with each metric classified as `improved`, `regressed`, `flat`, or `insufficient_data`. Active plans report in-flight progress from the plan's start; closed plans measure from the close date.

<ParamField query="window_days" type="integer" default="14">
  Length in days of both the before and after comparison windows (1–90).
</ParamField>

<ParamField query="fcr_window_hours" type="integer" default="24">
  Look-ahead used by the First-Contact-Resolution calculation (1–168).
</ParamField>

<RequestExample>
  <CodeGroup>
    ```bash cURL theme={null}
    curl -X POST "https://api.orbit.devotel.io/api/v1/voice/coaching-plans" \
      -H "X-API-Key: dv_live_sk_your_key_here" \
      -H "Content-Type: application/json" \
      -d '{
      "agent_user_id": "usr_agent_42",
      "goal": "Reduce average handle time by 20% over four weeks.",
      "due_at": "2026-08-01T00:00:00Z"
    }'
    # Later, close it
    curl -X POST "https://api.orbit.devotel.io/api/v1/voice/coaching-plans/cplan_7a1/close" \
      -H "X-API-Key: dv_live_sk_your_key_here" \
      -H "Content-Type: application/json" \
      -d '{ "status": "completed", "outcome": "met" }'
    # Read the effectiveness report
    curl -X GET "https://api.orbit.devotel.io/api/v1/voice/coaching-plans/cplan_7a1/effectiveness?window_days=14" \
      -H "X-API-Key: dv_live_sk_your_key_here"
    ```

    ```typescript Node.js theme={null}
    const plan = await orbit.voice.coachingPlans.create({
      agent_user_id: 'usr_agent_42',
      goal: 'Reduce average handle time by 20% over four weeks.',
      due_at: '2026-08-01T00:00:00Z',
    })

    await orbit.voice.coachingPlans.close(plan.data.id, { status: 'completed', outcome: 'met' })

    const report = await orbit.voice.coachingPlans.effectiveness(plan.data.id, { window_days: 14 })
    console.log(report.data.kpis)
    ```

    ```python Python theme={null}
    import os, requests
    headers = {"X-API-Key": os.environ["ORBIT_API_KEY"], "Content-Type": "application/json"}
    r = requests.post("https://api.orbit.devotel.io/api/v1/voice/coaching-plans", headers=headers, json={
      "agent_user_id": "usr_agent_42",
      "goal": "Reduce average handle time by 20% over four weeks.",
      "due_at": "2026-08-01T00:00:00Z"
    })
    plan_id = r.json()["data"]["id"]
    requests.get(f"https://api.orbit.devotel.io/api/v1/voice/coaching-plans/{plan_id}/effectiveness",
                 headers=headers, params={"window_days": 14})
    ```
  </CodeGroup>
</RequestExample>

***

## Real-Time Agent Assist

Stream real-time guidance to an agent while a call is live. An LLM reads the running transcript and emits cards — a suggested reply, relevant knowledge-base articles, and a next-best action — over [Server-Sent Events](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events).

### Agent-Assist Stream

`GET /api/v1/voice/calls/{callId}/agent-assist/stream`

The response is an SSE stream (`Content-Type: text/event-stream`). It opens with an `event: connected` handshake frame carrying the current call status, followed by an `event: status` frame. Each new suggestion arrives as an `event: assist` frame whose `data` is the card JSON. `: ping` comment lines keep the connection alive, and an `event: end` frame is sent when the call completes.

<RequestExample>
  <CodeGroup>
    ```bash cURL theme={null}
    curl -N -X GET "https://api.orbit.devotel.io/api/v1/voice/calls/call_abc123/agent-assist/stream" \
      -H "X-API-Key: dv_live_sk_your_key_here" \
      -H "Accept: text/event-stream"
    ```

    ```typescript Node.js theme={null}
    const res = await fetch(
      'https://api.orbit.devotel.io/api/v1/voice/calls/call_abc123/agent-assist/stream',
      { headers: { 'X-API-Key': process.env.ORBIT_API_KEY!, Accept: 'text/event-stream' } },
    )

    const reader = res.body!.getReader()
    const decoder = new TextDecoder()
    for (;;) {
      const { value, done } = await reader.read()
      if (done) break
      process.stdout.write(decoder.decode(value))
    }
    ```
  </CodeGroup>
</RequestExample>

<ResponseExample>
  ```text SSE theme={null}
  id: 1
  event: connected
  data: {"call_id":"call_abc123","status":"in_progress"}

  id: 2
  event: assist
  data: {"kind":"suggested_reply","text":"Offer the annual plan — it saves them 15%.","kb_articles":[{"title":"Annual billing","url":"..."}]}
  ```
</ResponseExample>

***

## Real-Time Supervisor Assist

The supervisor-facing companion to agent assist. Instead of a suggested reply, it surfaces an oversight action card — `intervene`, `escalate`, `offer`, or `monitor` — to a supervisor watching a live call, so they can decide in the moment whether to whisper-coach, barge in, relay a resource, or keep watching. Requires an owner, admin, or supervisor role; supervisors see only calls on queues they supervise.

### Supervisor-Assist Stream

`GET /api/v1/voice/supervisor/calls/{callId}/assist/stream`

Same SSE shape as agent assist: an `event: connected` handshake, an `event: status` frame, `event: assist` action cards as they are generated, `: ping` keep-alives, and a closing `event: end` frame.

<RequestExample>
  <CodeGroup>
    ```bash cURL theme={null}
    curl -N -X GET "https://api.orbit.devotel.io/api/v1/voice/supervisor/calls/call_abc123/assist/stream" \
      -H "X-API-Key: dv_live_sk_your_key_here" \
      -H "Accept: text/event-stream"
    ```

    ```typescript Node.js theme={null}
    const res = await fetch(
      'https://api.orbit.devotel.io/api/v1/voice/supervisor/calls/call_abc123/assist/stream',
      { headers: { 'X-API-Key': process.env.ORBIT_API_KEY!, Accept: 'text/event-stream' } },
    )
    // consume res.body as an SSE stream (see Agent Assist example)
    ```
  </CodeGroup>
</RequestExample>

***

## Real-Time Compliance Monitor

Score a live call against a compliance ruleset as it happens. An LLM evaluates the running transcript for mandatory disclosures (recording-consent notice, mini-Miranda, FDCPA language), profanity, and script adherence, and streams per-rule findings to a monitoring panel. Critical and warning breaches also raise a `voice.compliance.violation` event so a supervisor is alerted even without the panel open.

### Compliance-Monitor Stream

`GET /api/v1/voice/calls/{callId}/compliance/stream`

An SSE stream that opens with an `event: connected` handshake and an `event: status` frame, then emits an `event: compliance` frame each time the monitor scores a rule. `: ping` comments keep the connection alive; an `event: end` frame closes it when the call completes.

Each `event: compliance` frame carries a `findings` array — one entry per rule, each with a `kind` (`disclosure`, `profanity`, or `script_adherence`), a `status` (`satisfied`, `pending`, or `violated`), a `severity` (`info`, `warning`, or `critical`), and a short `detail` (plus an optional transcript `excerpt`). The frame also carries an `overall_adherence` score in `[0, 1]` and an `escalated` array listing the `rule_id`s that raised a `voice.compliance.violation` supervisor alert on that tick.

<RequestExample>
  <CodeGroup>
    ```bash cURL theme={null}
    curl -N -X GET "https://api.orbit.devotel.io/api/v1/voice/calls/call_abc123/compliance/stream" \
      -H "X-API-Key: dv_live_sk_your_key_here" \
      -H "Accept: text/event-stream"
    ```

    ```typescript Node.js theme={null}
    const res = await fetch(
      'https://api.orbit.devotel.io/api/v1/voice/calls/call_abc123/compliance/stream',
      { headers: { 'X-API-Key': process.env.ORBIT_API_KEY!, Accept: 'text/event-stream' } },
    )
    // consume res.body as an SSE stream (see Agent Assist example)
    ```
  </CodeGroup>
</RequestExample>

<ResponseExample>
  ```text SSE theme={null}
  id: 1
  event: connected
  data: {"call_id":"call_abc123","status":"in_progress"}

  id: 2
  event: compliance
  data: {"id":"cmp_01","call_id":"call_abc123","generated_at":"2026-07-03T10:15:04Z","model":"gpt-4o-mini","overall_adherence":0.72,"escalated":["mini_miranda"],"findings":[{"rule_id":"recording_consent","kind":"disclosure","status":"satisfied","severity":"critical","detail":"Recording-consent disclosure read in the opening line."},{"rule_id":"mini_miranda","kind":"disclosure","status":"violated","severity":"critical","detail":"Mini-Miranda not stated before discussing the debt.","excerpt":"So you owe $420 on the account…"},{"rule_id":"profanity","kind":"profanity","status":"satisfied","severity":"info","detail":"No profanity or abusive language detected."},{"rule_id":"script_adherence","kind":"script_adherence","status":"violated","severity":"warning","detail":"Skipped the identity-verification step in the approved flow.","excerpt":"Let's just get you set up with a plan."}]}
  ```
</ResponseExample>

***

## Dial Plan

A dial plan applies PBX-style digit-translation rules to outbound destinations before the call is placed — strip an access prefix, prepend a country code, or rewrite a short code to a full number. The plan applies across your whole organization and requires an owner or admin role.

### Get the Dial Plan

`GET /api/v1/voice/dial-plan`

Returns the current plan. When none is configured, returns `{ "enabled": false, "rules": [] }` so you can render an empty state without handling a 404.

### Replace the Dial Plan

`PUT /api/v1/voice/dial-plan`

Replaces the plan in full. The response echoes the normalized plan — no-op rules are dropped and defaults are applied — so it reflects exactly what the translation engine will run.

<ParamField body="enabled" type="boolean" required>
  Whether the plan is active. When `false`, destinations are dialed unchanged.
</ParamField>

<ParamField body="rules" type="object[]" required>
  Ordered translation rules (up to 100). The first matching rule is applied.

  <Expandable title="rule">
    <ParamField body="rules[].match" type="string" required>
      The pattern to match against the dialed digits. Dialable characters only (`+`, `0`–`9`, `*`, `#`), up to 32 characters.
    </ParamField>

    <ParamField body="rules[].match_type" type="string" default="prefix">
      How `match` is compared: `prefix` or `exact`.
    </ParamField>

    <ParamField body="rules[].name" type="string">
      Optional label for the rule (up to 128 characters).
    </ParamField>

    <ParamField body="rules[].min_length" type="integer">
      Only apply when the dialed number has at least this many digits (1–64).
    </ParamField>

    <ParamField body="rules[].max_length" type="integer">
      Only apply when the dialed number has at most this many digits (1–64).
    </ParamField>

    <ParamField body="rules[].strip_digits" type="integer">
      Remove this many leading digits before dialing (0–20).
    </ParamField>

    <ParamField body="rules[].prepend" type="string">
      Digits to prepend after stripping (up to 20 dialable characters).
    </ParamField>

    <ParamField body="rules[].replace_with" type="string">
      Replace the matched number outright with this value (up to 32 dialable characters).
    </ParamField>
  </Expandable>
</ParamField>

<Note>
  Each rule must do something — set `replace_with`, or set `strip_digits`/`prepend`. A rule that would leave the number unchanged is rejected with `400`.
</Note>

<RequestExample>
  <CodeGroup>
    ```bash cURL theme={null}
    curl -X PUT "https://api.orbit.devotel.io/api/v1/voice/dial-plan" \
      -H "X-API-Key: dv_live_sk_your_key_here" \
      -H "Content-Type: application/json" \
      -d '{
      "enabled": true,
      "rules": [
        { "name": "Strip 9 access code", "match": "9", "match_type": "prefix", "strip_digits": 1 },
        { "name": "Local to E.164", "match": "0", "match_type": "prefix", "strip_digits": 1, "prepend": "+44" }
      ]
    }'
    ```

    ```typescript Node.js theme={null}
    await orbit.voice.dialPlan.update({
      enabled: true,
      rules: [
        { name: 'Strip 9 access code', match: '9', match_type: 'prefix', strip_digits: 1 },
        { name: 'Local to E.164', match: '0', match_type: 'prefix', strip_digits: 1, prepend: '+44' },
      ],
    })
    ```

    ```python Python theme={null}
    import os, requests
    headers = {"X-API-Key": os.environ["ORBIT_API_KEY"], "Content-Type": "application/json"}
    requests.put("https://api.orbit.devotel.io/api/v1/voice/dial-plan", headers=headers, json={
      "enabled": True,
      "rules": [
        {"name": "Strip 9 access code", "match": "9", "match_type": "prefix", "strip_digits": 1},
        {"name": "Local to E.164", "match": "0", "match_type": "prefix", "strip_digits": 1, "prepend": "+44"}
      ]
    })
    ```
  </CodeGroup>
</RequestExample>

***

## Dialing Restrictions

Dialing restrictions are a class-of-service policy: they cap which categories of number each user is allowed to dial outbound — toll-free, domestic, international, or premium-rate. The policy applies across your organization and requires an owner or admin role.

### Get Dialing Restrictions

`GET /api/v1/voice/dialing-restrictions`

Returns the current policy, or defaults (`enabled: false`) when none is set.

### Replace Dialing Restrictions

`PUT /api/v1/voice/dialing-restrictions`

<ParamField body="enabled" type="boolean" required>
  Whether the policy is enforced. When `false`, no class-of-service check runs.
</ParamField>

<ParamField body="home_country" type="string">
  ISO 3166 alpha-2 code (e.g. `US`, `GB`) used to classify destinations as domestic vs international.
</ParamField>

<ParamField body="premium_prefixes" type="string[]">
  Extra premium-rate prefixes to treat as the `premium` class, in `+` E.164 form (up to 100).
</ParamField>

<ParamField body="default_allowed_classes" type="string[]">
  Classes any user may dial unless overridden. Values: `tollfree`, `domestic`, `international`, `premium`.
</ParamField>

<ParamField body="users" type="object">
  Per-user overrides keyed by user id. Each value is `{ "allowed_classes": [...] }` listing the classes that user may dial.
</ParamField>

<RequestExample>
  <CodeGroup>
    ```bash cURL theme={null}
    curl -X PUT "https://api.orbit.devotel.io/api/v1/voice/dialing-restrictions" \
      -H "X-API-Key: dv_live_sk_your_key_here" \
      -H "Content-Type: application/json" \
      -d '{
      "enabled": true,
      "home_country": "US",
      "default_allowed_classes": ["tollfree", "domestic"],
      "users": {
        "usr_agent_42": { "allowed_classes": ["tollfree", "domestic", "international"] }
      }
    }'
    ```

    ```typescript Node.js theme={null}
    await orbit.voice.dialingRestrictions.update({
      enabled: true,
      home_country: 'US',
      default_allowed_classes: ['tollfree', 'domestic'],
      users: {
        usr_agent_42: { allowed_classes: ['tollfree', 'domestic', 'international'] },
      },
    })
    ```

    ```python Python theme={null}
    import os, requests
    headers = {"X-API-Key": os.environ["ORBIT_API_KEY"], "Content-Type": "application/json"}
    requests.put("https://api.orbit.devotel.io/api/v1/voice/dialing-restrictions", headers=headers, json={
      "enabled": True,
      "home_country": "US",
      "default_allowed_classes": ["tollfree", "domestic"],
      "users": {"usr_agent_42": {"allowed_classes": ["tollfree", "domestic", "international"]}}
    })
    ```
  </CodeGroup>
</RequestExample>

***

## Voicemail Greeting

Each user manages their own personal voicemail greeting — upload an audio file, synthesize one from text, fetch the current greeting, or remove it to fall back to your tenant default. Any authenticated user can manage their own greeting; no admin role is required.

### Get the Current Greeting

`GET /api/v1/voice/voicemail/greeting`

Returns `voicemail_greeting_url` and `voicemail_greeting_uploaded_at` (both `null` when no personal greeting is set).

### Upload a Greeting

`POST /api/v1/voice/voicemail/greeting`

Send `multipart/form-data` with a single audio file. MP3 and WAV are accepted, up to 2 MB. Non-audio uploads return `415`; a file over the limit returns `413`.

### Synthesize a Greeting from Text

`POST /api/v1/voice/voicemail/greeting/tts`

<ParamField body="text" type="string" required>
  The greeting script to speak (1–500 characters).
</ParamField>

<ParamField body="voice_id" type="string">
  Optional voice to synthesize with.
</ParamField>

<ParamField body="language" type="string">
  Optional BCP-47 language code such as `en` or `en-US`.
</ParamField>

### Delete the Greeting

`DELETE /api/v1/voice/voicemail/greeting`

Removes your personal greeting and reverts to the tenant default.

<RequestExample>
  <CodeGroup>
    ```bash cURL theme={null}
    # Upload an audio file
    curl -X POST "https://api.orbit.devotel.io/api/v1/voice/voicemail/greeting" \
      -H "X-API-Key: dv_live_sk_your_key_here" \
      -F "file=@greeting.mp3;type=audio/mpeg"

    # Or synthesize from text
    curl -X POST "https://api.orbit.devotel.io/api/v1/voice/voicemail/greeting/tts" \
      -H "X-API-Key: dv_live_sk_your_key_here" \
      -H "Content-Type: application/json" \
      -d '{ "text": "You have reached Alex. Please leave a message after the tone.", "language": "en-US" }'

    # Revert to the tenant default
    curl -X DELETE "https://api.orbit.devotel.io/api/v1/voice/voicemail/greeting" \
      -H "X-API-Key: dv_live_sk_your_key_here"
    ```

    ```python Python theme={null}
    import os, requests
    headers = {"X-API-Key": os.environ["ORBIT_API_KEY"]}
    # Upload
    with open("greeting.mp3", "rb") as f:
        requests.post("https://api.orbit.devotel.io/api/v1/voice/voicemail/greeting",
                      headers=headers, files={"file": ("greeting.mp3", f, "audio/mpeg")})
    # Synthesize
    requests.post("https://api.orbit.devotel.io/api/v1/voice/voicemail/greeting/tts",
                  headers={**headers, "Content-Type": "application/json"},
                  json={"text": "You have reached Alex. Please leave a message after the tone.", "language": "en-US"})
    ```
  </CodeGroup>
</RequestExample>

***

## Post-Call Surveys (CSAT & NPS)

Configure IVR post-call surveys and read their results. After a qualifying call ends, the caller is offered a single-digit survey — a 1–5 CSAT rating or a 0–10 NPS rating. The endpoints below manage the survey configuration and return the aggregated analytics; the survey itself is delivered by the voice platform.

### CSAT Analytics

`GET /api/v1/voice/csat/surveys/{id}/analytics`

Returns totals, average score (1–5), satisfaction percentage (share scoring 4 or 5), a per-score breakdown, and a daily trend.

<ParamField path="id" type="string" required>
  The survey id.
</ParamField>

<ParamField query="window" type="string" default="30d">
  Reporting window: `7d`, `30d`, `90d`, or `all`.
</ParamField>

<ParamField query="agent_user_id" type="string">
  Optional filter to one agent's calls.
</ParamField>

### CSAT Configuration

`GET /api/v1/voice/csat/surveys/{id}/config`

`PATCH /api/v1/voice/csat/surveys/{id}/config`

Read or update the survey's delivery settings. `PATCH` accepts any of the fields below (at least one is required).

<ParamField body="enabled" type="boolean">
  Whether the survey is offered after calls.
</ParamField>

<ParamField body="delivery_mode" type="string">
  How the survey is delivered: `ivr_post_call` (spoken at the end of the call) or `message`.
</ParamField>

<ParamField body="ivr_config" type="object">
  IVR prompt and gating settings.

  <Expandable title="ivr_config">
    <ParamField body="ivr_config.prompt_text" type="string">
      The spoken prompt (up to 500 characters).
    </ParamField>

    <ParamField body="ivr_config.prompt_audio_url" type="string">
      Optional HTTPS URL to a pre-recorded prompt, played instead of `prompt_text`.
    </ParamField>

    <ParamField body="ivr_config.digit_timeout_ms" type="integer">
      How long to wait for a keypress (1000–30000 ms).
    </ParamField>

    <ParamField body="ivr_config.max_attempts" type="integer">
      Retries on an invalid or missing digit (1–3).
    </ParamField>

    <ParamField body="ivr_config.thank_you_prompt" type="string">
      Spoken after a valid response.
    </ParamField>

    <ParamField body="ivr_config.only_if_human_answered" type="boolean">
      Skip the survey when the call was answered by a machine or fax.
    </ParamField>

    <ParamField body="ivr_config.only_if_min_duration_sec" type="integer">
      Skip the survey for calls shorter than this (0–3600 seconds).
    </ParamField>

    <ParamField body="ivr_config.sample_rate_pct" type="integer">
      Offer the survey to this percentage of eligible calls (1–100).
    </ParamField>

    <ParamField body="ivr_config.only_if_disposition_in" type="string[]">
      Only survey calls whose final disposition code is in this list.
    </ParamField>

    <ParamField body="ivr_config.exclude_disposition_in" type="string[]">
      Never survey calls whose final disposition code is in this list (takes precedence over the allowlist).
    </ParamField>
  </Expandable>
</ParamField>

### NPS Analytics

`GET /api/v1/voice/nps/surveys/{id}/analytics`

Returns totals, the NPS score (promoters minus detractors, −100 to +100), the promoter/passive/detractor breakdown, and a daily trend. Accepts the same `window` and `agent_user_id` query parameters as CSAT analytics.

### NPS Configuration

`GET /api/v1/voice/nps/surveys/{id}/config`

`PATCH /api/v1/voice/nps/surveys/{id}/config`

Read or update the NPS survey settings. The body matches the CSAT config above (`enabled`, `delivery_mode`, `ivr_config`), with an NPS 0–10 prompt.

<RequestExample>
  <CodeGroup>
    ```bash cURL theme={null}
    # Read CSAT results for the last 30 days
    curl -X GET "https://api.orbit.devotel.io/api/v1/voice/csat/surveys/svy_abc123/analytics?window=30d" \
      -H "X-API-Key: dv_live_sk_your_key_here"

    # Enable the CSAT survey and set the prompt
    curl -X PATCH "https://api.orbit.devotel.io/api/v1/voice/csat/surveys/svy_abc123/config" \
      -H "X-API-Key: dv_live_sk_your_key_here" \
      -H "Content-Type: application/json" \
      -d '{
      "enabled": true,
      "delivery_mode": "ivr_post_call",
      "ivr_config": { "prompt_text": "Please rate this call from 1 to 5.", "sample_rate_pct": 50 }
    }'
    ```

    ```typescript Node.js theme={null}
    const analytics = await orbit.voice.csat.analytics('svy_abc123', { window: '30d' })

    await orbit.voice.csat.updateConfig('svy_abc123', {
      enabled: true,
      delivery_mode: 'ivr_post_call',
      ivr_config: { prompt_text: 'Please rate this call from 1 to 5.', sample_rate_pct: 50 },
    })
    ```

    ```python Python theme={null}
    import os, requests
    headers = {"X-API-Key": os.environ["ORBIT_API_KEY"], "Content-Type": "application/json"}
    requests.get("https://api.orbit.devotel.io/api/v1/voice/csat/surveys/svy_abc123/analytics",
                 headers=headers, params={"window": "30d"})
    requests.patch("https://api.orbit.devotel.io/api/v1/voice/csat/surveys/svy_abc123/config",
                   headers=headers, json={
      "enabled": True,
      "delivery_mode": "ivr_post_call",
      "ivr_config": {"prompt_text": "Please rate this call from 1 to 5.", "sample_rate_pct": 50}
    })
    ```
  </CodeGroup>
</RequestExample>

***

## Call Statuses

| Status        | Description                               |
| ------------- | ----------------------------------------- |
| `initiating`  | Call is being set up                      |
| `ringing`     | Destination phone is ringing              |
| `in_progress` | Call is connected and active              |
| `completed`   | Call ended normally                       |
| `failed`      | Call could not be connected               |
| `busy`        | Destination returned busy signal          |
| `no_answer`   | Destination did not answer within timeout |
| `cancelled`   | Call was cancelled before connection      |
