---
summary: Every ScaiDial HTTP endpoint, grouped by surface. Admin under /v1/modules/scaidial/,
  end-user under /v1/modules/scaidial/me/.
title: API Reference
path: reference/api
status: published
---

ScaiDial exposes three HTTP surfaces:

- **Admin** (`/v1/modules/scaidial/`) — tenant-wide configuration and observability. Requires module permissions documented in [permissions](./permissions).
- **Portal** (`/v1/modules/scaidial/me/`) — end-user self-service. No special permission; per-row access is gated by `ExtensionGrant`.
- **Internal** (`/v1/modules/scaidial/sip/...`) — called by livekit-sip with a shared secret. Not exposed to callers; documented for completeness in the integration guide.

All routes return the ScaiGrid envelope: `{"data": ..., "status": "ok"}` or `{"error": {"code": ..., "message": ..., "details": ...}}`.

---

## Trunks

### `GET /trunks`

List trunks for the caller's tenant.

### `POST /trunks`

Create a trunk. Body:

| Field | Type | Notes |
|---|---|---|
| `name` | string | Operator-visible label |
| `sip_server_address` | string | FQDN or IP of the carrier's SIP server |
| `sip_server_port` | int | Usually 5060 (UDP/TCP) or 5061 (TLS) |
| `sip_transport` | string | `udp` / `tcp` / `tls` |
| `sip_auth_mode` | string | `credentials` or `ip` |
| `sip_auth_username` | string | When mode=credentials |
| `sip_auth_password` | string | When mode=credentials |
| `sip_inbound_numbers` | string[] | E.164 numbers the carrier delivers to this trunk |
| `sip_media_encryption` | string | `disabled` / `allowed` / `required` |
| `caller_id_e164` | string | Default outbound caller-ID |

Returns the created trunk with `sync_status="pending"`. Poll until `synced` (or `error`).

### `PATCH /trunks/{id}`

Update a trunk. Same body as POST; all fields optional. Changes trigger a re-sync.

### `DELETE /trunks/{id}`

Delete a trunk. Removes the registration on livekit-sip. Fails if any DID references the trunk.

### `POST /trunks/{id}/resync`

Force a re-sync after an error. Useful when you've fixed credentials on the carrier side.

### `POST /trunks/probe`

Test a trunk's connectivity without persisting it. Body is the same shape as POST. Returns `{"ok": bool, "detail": string}`.

---

## DIDs

### `GET /dids`

List DIDs for the caller's tenant.

### `POST /dids`

Create a DID. Body:

| Field | Type | Notes |
|---|---|---|
| `e164` | string | E.164 number |
| `trunk_id` | UUID | Which trunk delivers this number |
| `dialplan_id` | UUID | Which dialplan handles incoming calls |
| `enabled` | bool | Default `true` |

### `PATCH /dids/{id}`

Partial update. `enabled` is the toggle the UI flips inline.

### `DELETE /dids/{id}`

Delete a DID.

---

## Extensions

### `GET /extensions`

List extensions for the caller's tenant.

### `POST /extensions`

Create an extension. Body:

| Field | Type | Notes |
|---|---|---|
| `number` | string | Internal number (e.g. `1001`) |
| `type` | string | `wave` / `bot` / `voicemail` / `external` / `sip_endpoint` / `desk_queue` |
| `target_ref` | string | For `wave`: user ID; for `bot`: bot ID; for `voicemail`: box ID; for `external`: E.164; for `sip_endpoint`: SIP URI |
| `display_name` | string | Operator-visible label |
| `ring_timeout_s` | int | Default 30; max 600 |
| `timeout_action` | string | What happens on no-answer: `voicemail` / `ring_extension` / `hangup` |
| `timeout_forward_to` | string | When `timeout_action=ring_extension`, the next extension's number |
| `outbound_caller_id_e164` | string | Override the trunk's default |

### `PATCH /extensions/{id}`

Partial update.

### `DELETE /extensions/{id}`

Delete an extension. Fails if any dialplan rule references it.

### `GET /extensions/{id}/grants`

List grants (who has access to this extension).

### `POST /extensions/{id}/grants`

Create a grant.

| Field | Type | Notes |
|---|---|---|
| `user_id` | UUID | Either this or `group_id` |
| `group_id` | UUID | Either this or `user_id` |
| `role` | string | `owner` / `manage` / `answer` / `observe` |

### `DELETE /extensions/{id}/grants/{grant_id}`

Remove a grant.

---

## Dialplans

### `GET /dialplans`

List dialplans for the caller's tenant.

### `POST /dialplans`

Create a dialplan. Body: `{"name": string}`.

### `PATCH /dialplans/{id}`

Rename a dialplan.

### `DELETE /dialplans/{id}`

Delete. Fails if any DID references it.

### `GET /dialplans/{id}/rules`

List rules for a dialplan, ordered by priority.

### `POST /dialplans/{id}/rules`

Create a rule. Body:

| Field | Type | Notes |
|---|---|---|
| `priority` | int | Lower fires first; 0-999 |
| `match_type` | string | See [dialplans](../concepts/dialplans) |
| `match_params` | object | Per-type — see dialplans concepts |
| `action_type` | string | See [dialplans](../concepts/dialplans) |
| `action_params` | object | Per-type — see dialplans concepts |
| `enabled` | bool | Default `true` |

### `PATCH /dialplans/{id}/rules/{rule_id}`

Partial update.

### `DELETE /dialplans/{id}/rules/{rule_id}`

Delete a rule.

### `GET /dialplan-pickers`

One fetch, multiple lists — used by the visual builder. Returns `{match_types, action_types, dids, extensions, bots, users, voicemail_boxes}`.

---

## Active calls

### `GET /calls/active`

List calls in state `ringing`, `active`, or `held`. Each call includes its current legs.

### `GET /calls/{id}`

Single call detail.

### `GET /calls/history`

Paginated history of ended calls. Filters:

- `extension_id` — calls where any leg touched this extension
- `from_e164` — substring match on caller
- `end_reason` — exact match
- `started_after` / `started_before` — ISO-8601
- `limit` — capped at 500
- `cursor_started_at` — keyset pagination

Returns `{items: [...], next_cursor_started_at: string | null}`.

### Per-leg controls

- `POST /legs/{id}/hangup` — end the leg
- `POST /legs/{id}/hold` — mute the leg's audio track
- `POST /legs/{id}/unhold` — restore
- `POST /legs/{id}/transfer` — blind transfer. Body: `{to_extension, mode: "blind"}`. 409 on attended (not yet implemented) or carrier-rejected REFER.

---

## Voicemail

### `GET /voicemail`

List voicemail messages. Query: `extension_id` (filter), `only_unheard` (bool), `limit`.

### `GET /voicemail/{id}/audio`

Stream the recording bytes as `audio/wav`. Browsers can play this directly via `<audio src>`.

### `POST /voicemail/{id}/listened`

Mark listened. Idempotent.

### `DELETE /voicemail/{id}`

Delete a voicemail.

---

## Forward rules

### `GET /extensions/{id}/forward-rules`

List forward rules on an extension.

### `POST /extensions/{id}/forward-rules`

Create a rule. Body:

| Field | Type | Notes |
|---|---|---|
| `condition` | string | `always` / `busy` / `no_answer` / `unavailable` |
| `forward_to` | string | E.164 or SIP URI |
| `priority` | int | Lower fires first |
| `enabled` | bool |  |

### `DELETE /extensions/{id}/forward-rules/{rule_id}`

Delete.

---

## Tenant policy

### `GET /policy`

Read the tenant's policy. Returns defaults if no row exists.

```json
{"voicemail_transcript_enabled": false}
```

### `PATCH /policy`

Update. Only fields explicitly sent are touched. Stamps `updated_by_user_id` automatically.

---

## Network info

### `GET /network-info`

The four-tuple a carrier needs for whitelisting:

```json
{
  "egress": {"v4": "203.0.113.5", "v6": "2001:db8::1"},
  "sip": {"port": 5060, "transports": ["UDP", "TCP"]},
  "rtp": {"port_start": 10000, "port_end": 10200, "transport": "UDP"},
  "guidance": "..."
}
```

---

## End-user portal (`/me/...`)

Distinct surface for tenant users without admin permission. Per-row access is gated by `ExtensionGrant`: read operations need any grant role (`owner` / `manage` / `answer` / `observe`); edit operations need `owner`.

| Route | What |
|---|---|
| `GET /me/extensions` | My extensions, each with my role |
| `PATCH /me/extensions/{id}` | Edit personal config (DND, ring timeout, display name, voicemail greeting). Owner-only. |
| `GET /me/voicemail` | Voicemail across my owned/managed extensions |
| `GET /me/voicemail/{id}/audio` | Stream a voicemail |
| `POST /me/voicemail/{id}/listened` | Mark listened |
| `DELETE /me/voicemail/{id}` | Delete a voicemail |
| `GET /me/extensions/{id}/forward-rules` | List my forward rules (owner-only) |
| `POST /me/extensions/{id}/forward-rules` | Add a forward rule (owner-only) |
| `DELETE /me/extensions/{id}/forward-rules/{rule_id}` | Delete a forward rule |
| `GET /me/calls/history` | Calls I placed or that touched any of my extensions |
| `POST /me/click-to-call` | Originate a call. Body `{to, from_extension_id}`. Returns `{call_id, leg_id, livekit_url, livekit_token, room_name}` — the connection bundle for the browser softphone. |
