---
audience: developer
summary: Initiate, join, leave, record, end. Backed by LiveKit.
title: Calls API
path: reference/api/calls
status: published
---

# Calls API

10 endpoints. ScaiWave delegates SFU duties to LiveKit; this API
mints LiveKit tokens, tracks call state, and handles recordings.

| Method | Path | Purpose |
|---|---|---|
| `POST` | `/v1/calls/initiate` | Start a call in a room. |
| `POST` | `/v1/calls/{call_id}/join` | Join an in-flight call. |
| `POST` | `/v1/calls/{call_id}/leave` | Leave. |
| `POST` | `/v1/calls/{call_id}/reject` | Decline a ringing call. |
| `POST` | `/v1/calls/{call_id}/end` | End the call for everyone (initiator or `power ≥ 50`). |
| `GET` | `/v1/calls/{call_id}` | Call state. |
| `POST` | `/v1/calls/{call_id}/recording/start` | Start recording (admin). |
| `POST` | `/v1/calls/{call_id}/recording/stop` | Stop. |
| `GET` | `/v1/calls/{call_id}/recordings` | List finished recordings as media references. |
| `POST` | `/v1/calls/webhook` | LiveKit webhook callback (server-to-server). |

## POST /v1/calls/initiate

```jsonc
{
  "room_id": "<scaiwave-room-id>",
  "type": "audio"        // or "video"
}
```

Returns a LiveKit token and call id:

```jsonc
{
  "data": {
    "call_id": "call-…",
    "livekit_url": "wss://livekit.example.com",
    "livekit_token": "eyJ…",
    "livekit_room": "scaiwave-<call_id>"
  }
}
```

The client connects to LiveKit with that token; LiveKit handles
media. Other room members get a `swp.call.invite` WebSocket event
with a ringing UI.

## POST /v1/calls/{call_id}/join

```bash
curl -X POST "$BASE/v1/calls/$CALL_ID/join" \
  -H "Authorization: Bearer $TOKEN"
```

Returns a token scoped to this caller. The same LiveKit room
already exists.

## Recordings

```bash
curl -X POST "$BASE/v1/calls/$CALL_ID/recording/start" \
  -H "Authorization: Bearer $TOKEN" \
  -d '{"audio": true, "video": true, "transcript": true}'
```

LiveKit egress streams to MinIO under
`recordings/<call_id>/<timestamp>.mp4`. Once egress finishes, the
recording is registered as a media entry and attached to the
ScaiWave call. If `transcript: true`, an async transcription job
runs and posts the result back to the room.

## Webhook

LiveKit POSTs back when room state changes (participant joined /
left, egress finished). Signed via LiveKit's HMAC scheme; ScaiWave
verifies before processing.

You don't call this; LiveKit does. Configure the URL in your
LiveKit project's webhook settings.

## State

```json
{
  "data": {
    "id": "call-…",
    "room_id": "room-…",
    "type": "video",
    "status": "active",
    "started_at": "...",
    "ended_at": null,
    "initiator_id": "5e4d…",
    "participants": [
      { "participant_id": "5e4d…", "joined_at": "...", "video_on": true, "audio_on": true }
    ],
    "recordings": []
  }
}
```

## Errors

- `SW_CALL_NOT_FOUND` / `SW_CALL_ENDED` — call no longer active.
- `SW_CALL_LIVEKIT_UNREACHABLE` — backend ops issue.
- `SW_CALL_FORBIDDEN` — caller isn't in the room.
- `SW_CALL_RECORDING_DISABLED` — tenant has recordings disabled.
