> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://docs.telekesher.dev/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.telekesher.dev/_mcp/server.

# Recording events

Recording events notify your application as a session or room
[Recording](/api/recording) moves through its lifecycle. Session Recordings can fail
their liveness check and can opt into customer speech activity events.

The per-command correlation field is `operation_uuid`, echoed from the
command's `202 Accepted` [Operation](/operation).

Session and room Recordings use the same lifecycle event names and the same
public `recording_uuid`. Session events carry `session_uuid`; room events carry
`room_id`.

## command.recording.start.accepted

Fired immediately after a recording start command is dispatched. Its `pull_url` fetches the whole recording so far and can be opened while recording is in progress. A session recording emits either [`recording.became_available`](#recordingbecame_available) or [`recording.failed`](#recordingfailed), normally within about 10 seconds. A room recording emits [`recording.became_available`](#recordingbecame_available) only when liveness is confirmed; otherwise it remains active without a failure webhook.

**Payload schema**

| Field            | Type   | Description                                                     |
| ---------------- | ------ | --------------------------------------------------------------- |
| `event`          | string | Always `"command.recording.start.accepted"`. **Required**       |
| `session_uuid`   | string | Session being recorded. *Session recordings*                    |
| `room_id`        | string | Room being recorded. *Room recordings*                          |
| `recording_uuid` | string | Recording identifier (`rec_<uuid-v4>`). **Required**            |
| `pull_url`       | string | URL for the whole recording so far. **Required**                |
| `expires_at`     | string | Exact RFC 3339 UTC expiration time for `pull_url`. **Required** |
| `operation_uuid` | string | Correlation ID of the recording start command. **Required**     |
| `timestamp`      | string | RFC 3339 / ISO 8601 UTC emission timestamp. **Required**        |

```json
{
  "event": "command.recording.start.accepted",
  "session_uuid": "acW68-f47ac10b-58cc-4372-a567-0e02b2c3d479",
  "recording_uuid": "rec_11111111-2222-4333-8444-555555555555",
  "pull_url": "https://recordings.example.com/c/opaque-signed-url-1",
  "expires_at": "2025-06-16T14:50:00.000Z",
  "operation_uuid": "a1b2c3d4-e5f6-4890-abcd-ef1234567890",
  "timestamp": "2025-06-15T14:50:00.000Z"
}
```

## recording.speech.started

Fired when customer speech is detected during a session recording started with
`enable_voice_activity_events: true`. The first event omits
`preceding_silence_duration_ms`; later events include the measured silence since
the preceding `recording.speech.ended`. `sensitivity` controls how quiet a sound
can be and still count as speech; `speech_duration_ms` controls how long speech
must continue before this event. Both trade faster or quieter detection against
more false starts. See the `recording/start` request schema for ranges and
recommended values.

| Field                           | Type    | Description                                                             |
| ------------------------------- | ------- | ----------------------------------------------------------------------- |
| `event`                         | string  | Always `"recording.speech.started"`. **Required**                       |
| `session_uuid`                  | string  | Session being recorded. **Required**                                    |
| `recording_uuid`                | string  | Recording identifier (`rec_<uuid-v4>`). **Required**                    |
| `operation_uuid`                | string  | Correlation ID of the originating recording start command. **Required** |
| `preceding_silence_duration_ms` | integer | Silence before speech resumed. Omitted for the first speech interval.   |
| `timestamp`                     | string  | RFC 3339 / ISO 8601 UTC transition timestamp. **Required**              |

## recording.speech.ended

Fired after customer audio remains below the configured threshold for
`silence_duration_ms`. Lower values report an end faster but can split natural
pauses; higher values tolerate pauses but delay this event.
It is never emitted before the first `recording.speech.started`, and neither
speech event is emitted after the recording stops or the session hangs up.

| Field            | Type   | Description                                                             |
| ---------------- | ------ | ----------------------------------------------------------------------- |
| `event`          | string | Always `"recording.speech.ended"`. **Required**                         |
| `session_uuid`   | string | Session being recorded. **Required**                                    |
| `recording_uuid` | string | Recording identifier (`rec_<uuid-v4>`). **Required**                    |
| `operation_uuid` | string | Correlation ID of the originating recording start command. **Required** |
| `timestamp`      | string | RFC 3339 / ISO 8601 UTC transition timestamp. **Required**              |

## recording.became\_available

Fired after [`command.recording.start.accepted`](#commandrecordingstartaccepted) once the gateway confirms that audio is reaching durable storage and is fetchable. Wait for this event when you need that guarantee before acting. It carries two URLs:

* `pull_url` — the **whole recording so far**, WAV-wrapped. It streams a live chunked tail while recording is active, then serves the completed file with HTTP Range support. Download this URL to keep a copy.
* `live_url` — a **non-seekable live tail stream** from the current position. A listener joins at the live edge, hears “now” forward, and does not replay the beginning. Use it to monitor an active recording.

For a session recording, liveness not confirmed within the validation window emits [`recording.failed`](#recordingfailed) instead. Room recordings emit `recording.became_available` only on success; they do not emit a liveness-failure webhook. `recording.became_available` and `recording.failed` are mutually exclusive outcomes for a session recording.

**Payload schema**

| Field            | Type   | Description                                                          |
| ---------------- | ------ | -------------------------------------------------------------------- |
| `event`          | string | Always `"recording.became_available"`. **Required**                  |
| `session_uuid`   | string | Session being recorded. *Session recordings*                         |
| `room_id`        | string | Room being recorded. *Room recordings*                               |
| `recording_uuid` | string | Recording identifier (`rec_<uuid-v4>`). **Required**                 |
| `pull_url`       | string | URL for the whole-so-far WAV. **Required**                           |
| `live_url`       | string | URL for the non-seekable live tail stream. **Required**              |
| `expires_at`     | string | Exact RFC 3339 UTC expiration time shared by both URLs. **Required** |
| `operation_uuid` | string | Correlation ID of the recording start command. **Required**          |
| `timestamp`      | string | RFC 3339 / ISO 8601 UTC emission timestamp. **Required**             |

```json
{
  "event": "recording.became_available",
  "session_uuid": "acW68-f47ac10b-58cc-4372-a567-0e02b2c3d479",
  "recording_uuid": "rec_11111111-2222-4333-8444-555555555555",
  "pull_url": "https://recordings.example.com/c/opaque-signed-url-1",
  "live_url": "https://recordings.example.com/c/opaque-signed-url-2",
  "expires_at": "2025-06-16T14:50:01.000Z",
  "operation_uuid": "a1b2c3d4-e5f6-4890-abcd-ef1234567890",
  "timestamp": "2025-06-15T14:50:01.000Z"
}
```

## recording.ended

Fired when an active recording is stopped. Session recordings also emit this event when the session hangs up while recording.

For both session and room recordings, `operation_uuid` identifies the
originating `recording/start` command when available. A
later stop response has its own UUID, which is not copied into this lifetime
event.

**Payload schema**

| Field            | Type    | Description                                                                                                       |
| ---------------- | ------- | ----------------------------------------------------------------------------------------------------------------- |
| `event`          | string  | Always `"recording.ended"`. **Required**                                                                          |
| `session_uuid`   | string  | Session that was recorded. *Session recordings*                                                                   |
| `room_id`        | string  | Room that was recorded. *Room recordings*                                                                         |
| `recording_uuid` | string  | Recording identifier (`rec_<uuid-v4>`). **Required**                                                              |
| `pull_url`       | string  | Fresh URL for the completed Recording. **Required**                                                               |
| `expires_at`     | string  | Exact RFC 3339 UTC expiration time for `pull_url`. **Required**                                                   |
| `operation_uuid` | string  | Correlation ID of the originating `recording/start` command for session and room recordings. *Present when known* |
| `duration`       | integer | Wall-clock recording duration in whole seconds. **Required**                                                      |
| `timestamp`      | string  | RFC 3339 / ISO 8601 UTC emission timestamp. **Required**                                                          |

```json
{
  "event": "recording.ended",
  "session_uuid": "acW68-f47ac10b-58cc-4372-a567-0e02b2c3d479",
  "recording_uuid": "rec_11111111-2222-4333-8444-555555555555",
  "pull_url": "https://recordings.example.com/c/opaque-signed-url-3",
  "expires_at": "2025-06-16T14:51:02.000Z",
  "operation_uuid": "a1b2c3d4-e5f6-4890-abcd-ef1234567890",
  "duration": 62,
  "timestamp": "2025-06-15T14:51:02.000Z"
}
```

## recording.failed

`recording.failed` is session-only. It fires when the post-dispatch liveness
check does not confirm that a new session Recording is reaching durable storage
within the validation window. This event is emitted instead of
[`recording.became_available`](#recordingbecame_available), with the error
`"recording unavailable"` and no usable URL. A room liveness timeout does not
emit this event or end the room Recording.

This failure completes the start-liveness check. A later stop or hangup can
still emit [`recording.ended`](#recordingended). If recording is not configured,
the start request returns `503` and no recording webhook is emitted.

**Payload schema**

| Field            | Type   | Description                                                                |
| ---------------- | ------ | -------------------------------------------------------------------------- |
| `event`          | string | Always `"recording.failed"`. **Required**                                  |
| `session_uuid`   | string | Session whose recording failed the liveness check. **Required**            |
| `recording_uuid` | string | Recording identifier (`rec_<uuid-v4>`). **Required**                       |
| `operation_uuid` | string | Correlation ID of the recording start command. **Required**                |
| `error`          | string | Currently `"recording unavailable"`. **Required**                          |
| `pull_url`       | string | Empty because no URL was minted; there is no validity window. **Required** |
| `timestamp`      | string | RFC 3339 / ISO 8601 UTC emission timestamp. **Required**                   |

```json
{
  "event": "recording.failed",
  "session_uuid": "acW68-f47ac10b-58cc-4372-a567-0e02b2c3d479",
  "recording_uuid": "rec_11111111-2222-4333-8444-555555555555",
  "operation_uuid": "a1b2c3d4-e5f6-4890-abcd-ef1234567890",
  "error": "recording unavailable",
  "pull_url": "",
  "timestamp": "2025-06-15T14:50:10.000Z"
}
```

## command.recording.mask.accepted

Fired when `recording/mask` is accepted for an active session recording. The command returns `409 Conflict` when no recording is active, and `500 Internal Server Error` if it cannot be accepted. This event confirms the masking request was accepted.

The recording stays active while audio writing is masked. A matching [`command.recording.unmask.accepted`](#commandrecordingunmaskaccepted) event is emitted when writing is requested to resume.

**Payload schema**

| Field            | Type   | Description                                                                               |
| ---------------- | ------ | ----------------------------------------------------------------------------------------- |
| `event`          | string | Always `"command.recording.mask.accepted"`. **Required**                                  |
| `session_uuid`   | string | Session identifier. **Required**                                                          |
| `recording_uuid` | string | Recording identifier (`rec_<uuid-v4>`). **Required**                                      |
| `operation_uuid` | string | Correlation ID echoed from the `202 Accepted` response for `recording/mask`. **Required** |
| `pull_url`       | string | Fresh URL for the Recording.                                                              |
| `expires_at`     | string | Exact RFC 3339 UTC expiration time. Present with `pull_url`.                              |
| `timestamp`      | string | RFC 3339 / ISO 8601 UTC emission timestamp. **Required**                                  |

```json
{
  "event": "command.recording.mask.accepted",
  "session_uuid": "acW68-f47ac10b-58cc-4372-a567-0e02b2c3d479",
  "recording_uuid": "rec_11111111-2222-4333-8444-555555555555",
  "operation_uuid": "a1b2c3d4-e5f6-4890-abcd-ef1234567890",
  "pull_url": "https://recordings.example.com/c/opaque-signed-url-4",
  "expires_at": "2025-06-16T14:50:30.000Z",
  "timestamp": "2025-06-15T14:50:30.000Z"
}
```

## command.recording.unmask.accepted

Fired when `recording/unmask` is accepted for an active session recording. This event confirms the request to resume audio writes was accepted. Calling `recording/unmask` when the active recording is already unmasked is harmless.

**Payload schema**

| Field            | Type   | Description                                                                                 |
| ---------------- | ------ | ------------------------------------------------------------------------------------------- |
| `event`          | string | Always `"command.recording.unmask.accepted"`. **Required**                                  |
| `session_uuid`   | string | Session identifier. **Required**                                                            |
| `recording_uuid` | string | Recording identifier (`rec_<uuid-v4>`). **Required**                                        |
| `operation_uuid` | string | Correlation ID echoed from the `202 Accepted` response for `recording/unmask`. **Required** |
| `pull_url`       | string | Fresh URL for the Recording.                                                                |
| `expires_at`     | string | Exact RFC 3339 UTC expiration time. Present with `pull_url`.                                |
| `timestamp`      | string | RFC 3339 / ISO 8601 UTC emission timestamp. **Required**                                    |

```json
{
  "event": "command.recording.unmask.accepted",
  "session_uuid": "acW68-f47ac10b-58cc-4372-a567-0e02b2c3d479",
  "recording_uuid": "rec_11111111-2222-4333-8444-555555555555",
  "operation_uuid": "b2c3d4e5-f6a7-4890-abcd-ef1234567890",
  "pull_url": "https://recordings.example.com/c/opaque-signed-url-5",
  "expires_at": "2025-06-16T14:50:45.000Z",
  "timestamp": "2025-06-15T14:50:45.000Z"
}
```

## Recording lifecycle

### Session recordings

1. A start command emits [`command.recording.start.accepted`](#commandrecordingstartaccepted) immediately.
2. The liveness check emits exactly one of [`recording.became_available`](#recordingbecame_available) or [`recording.failed`](#recordingfailed).
3. A stop command or session hangup emits [`recording.ended`](#recordingended).

In short: `command.recording.start.accepted` → (`recording.became_available` | `recording.failed`) → `recording.ended`.

`recording.became_available` and `recording.failed` are mutually exclusive. `recording.failed` and a later `recording.ended` are not mutually exclusive.

### Room recordings

1. A start command emits [`command.recording.start.accepted`](#commandrecordingstartaccepted) immediately.
2. A successful liveness check emits [`recording.became_available`](#recordingbecame_available); no event is emitted when the check does not confirm liveness.
3. A room recording stop emits [`recording.ended`](#recordingended).

Room recordings emit `recording.became_available` when liveness succeeds and do not emit `recording.failed`.

## Recording URLs

Fetch recording URLs without an `Authorization` header. Audio responses use `Content-Type: audio/wav`.

* `expires_at` is the exact expiry. The default validity window is 24 hours and may be configured differently.
* Fresh URLs are minted independently for lifecycle events. In particular, `recording.ended` provides a new default 24-hour download window after stop.
* Fetching a URL does not extend its validity.
* Invalid, expired, malformed, or altered URLs return `403 Forbidden`.
* Unknown recordings return `404 Not Found`; recordings removed after retention return `410 Gone`.
* `pull` streams the whole recording so far while active and supports HTTP Range after completion.
* `live` tails from the current offset and is not Range-capable.

Applications that need longer-lived audio must download the WAV before the chosen URL expires and store it themselves.

## Triggered by

* `command.recording.start.accepted` — `POST /v1/sessions/{uuid}/recording/start` and `POST /api/v1/rooms/{room_id}/playback/record`.
* `recording.became_available` — the successful liveness-check outcome after either start command.
* `recording.failed` — the unsuccessful liveness-check outcome after a session recording start only.
* `recording.ended` — `POST /v1/sessions/{uuid}/recording/stop`, session hangup while recording, and `POST /api/v1/rooms/{room_id}/playback/record/stop`.
* `command.recording.mask.accepted` — `POST /v1/sessions/{uuid}/recording/mask`.
* `command.recording.unmask.accepted` — `POST /v1/sessions/{uuid}/recording/unmask`.