> ## Documentation Index
> Fetch the complete documentation index at: https://docs.telepatia.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Live Sessions

> Stream consultation audio in real time and receive the generated record

A live session lets you stream a consultation while it happens. You create the session with one REST call, receive a WebSocket URL, send audio as the clinician speaks, and receive the generated medical record when the consultation ends.

Use this when you capture audio in real time. When you already hold a finished recording, use the [Consultations](/scribe-api/consultations) flow instead.

## How it works

1. `POST /v1/scribe-sessions/live` returns a WebSocket URL.
2. Connect to that URL and stream the audio as it is captured.
3. Send `stop_recording` when the consultation ends.
4. The `scribe_session.completed` [webhook](/scribe-api/webhooks) tells you the record is ready.
5. Fetch the record from [Medical Record Documents](/scribe-api/medical-record-documents).

## What the API key must carry

The key you use here needs all three of the following, or the call fails with `403`:

* The `scribe:write` permission, granted explicitly. A legacy key with no permissions claim works on some other endpoints but is refused here; regenerate the key to obtain one.
* The `doctor` role.
* A binding to an institution.

## Creating a session

The request body carries the consultation context: which consultation this is, which template to fill, which patient it belongs to, and any written context you already hold.

```bash theme={null}
curl -X POST https://scribe-api.telepatia.ai/v1/scribe-sessions/live \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "consultation": { "id": "CONSULT-12345", "modality": "IN_PERSON" },
    "template": { "id": "mrc_a1b2c3d4e5f6g7h8" },
    "patient": { "id": "sp_a1b2c3d4e5f6g7h8" },
    "context": {
      "notes": "Patient reports headache since Monday.",
      "pastMedicalHistory": "Hypertension diagnosed 2020."
    }
  }'
```

**Response:**

```json theme={null}
{
  "consultation": { "id": "CONSULT-12345" },
  "status": "created",
  "live": {
    "url": "wss://audio.telepatia.ai/v1/ws/audio/b0cf283f-ab53-51ea-8313-f9c90b38fd1f?token=eyJhbGciOi...",
    "expiresAt": "2026-08-13T22:14:49+00:00"
  }
}
```

Treat `live.url` as opaque and handle it as a secret. Connect to it exactly as returned, and do not build it yourself: the host, the path and the credential all change without notice.

The credential is checked only when the socket opens. A connection established before `expiresAt` keeps streaming past it, and a connection attempted after it is refused, so request a new session rather than reusing an expired URL.

### Request fields

| Field                        | Required | Notes                                                                                            |
| ---------------------------- | -------- | ------------------------------------------------------------------------------------------------ |
| `consultation.id`            | yes      | Your own consultation id. It is the idempotency key and the id you use to fetch the record later |
| `consultation.modality`      | no       | `IN_PERSON` or `TELEMEDICINE`. `DICTATION` is refused here: a dictated report has its own flow   |
| `template`                   | yes      | An id or the template JSON. See below                                                            |
| `patient`                    | yes      | An existing patient id, or the patient identity inline. See below                                |
| `context.notes`              | no       | Free text, up to 10000 characters, fed into record generation                                    |
| `context.pastMedicalHistory` | no       | Free text, up to 10000 characters, fed into record generation                                    |

### Choosing the template

`template` accepts **either an id or the template JSON itself**. Send exactly one of them.

| Field               | Send this when                                                                                                                                                                                |
| ------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `template.id`       | The template already exists in Telepatia. Use the public id of a [Smart Template](/scribe-api/smart-templates) (`mrc_...`) or a [Session Template](/scribe-api/session-templates) (`ssc_...`) |
| `template.sections` | Your system owns the template. Send the structure inline, with no registration step                                                                                                           |

Sending both, or neither, returns `400` with `param` set to `template`.

The inline form:

```json theme={null}
{
  "consultation": { "id": "CONSULT-12345", "modality": "IN_PERSON" },
  "template": {
    "sections": {
      "chiefComplaint": {
        "schema": { "type": "string", "instructions": "The patient's main complaint." },
        "systemPrompt": "Summarize the chief complaint."
      },
      "assessment": {
        "schema": { "type": "string", "instructions": "Clinical assessment and plan." },
        "systemPrompt": "Write the assessment and plan."
      }
    }
  },
  "patient": { "id": "sp_a1b2c3d4e5f6g7h8" }
}
```

The inline template is stored the first time you send it and reused afterwards: identical sections resolve to the same template by content hash, so sending it on every consultation creates no duplicates. The section format is the same one documented in [Smart Templates](/scribe-api/smart-templates), including nested objects, arrays and enums.

### Identifying the patient

`patient` also accepts **either an id or the identity inline**. Send exactly one.

```json theme={null}
{
  "patient": {
    "name": "John Doe",
    "document": { "country": "BR", "type": "CPF", "value": "123.456.789-00" }
  }
}
```

The inline form creates the patient when it does not exist yet, so you do not need to call the [Patients](/scribe-api/patients) API first.

### Retrying the same consultation

Calling the endpoint again with the same `consultation.id` while the session is still recording returns the same session with a **new** URL. That is the supported way to recover from a dropped connection. Do not compare the two URLs for equality, and do not cache the first one. Once the recording has stopped, the same id returns `409`.

## Sending audio

Connect to `live.url`. The server sends a `ready` message when it is prepared to receive audio.

Send audio as **binary WebSocket frames**. Two formats are accepted:

| Format  | Requirement                               |
| ------- | ----------------------------------------- |
| Raw PCM | 16 kHz, mono, 16-bit signed little-endian |
| Opus    | Inside an Ogg or WebM container           |

<Warning>
  Raw PCM is not inspected. If you send a different sample rate or channel count, the audio is accepted and transcribed incorrectly, with no error. A WAV file is accepted the same way: its header is not parsed, so the first bytes become a short burst of noise and the rest transcribes normally. Send the samples only, without the header.
</Warning>

Send roughly 100 ms of audio per frame, at the pace it is captured. A PCM payload under 1024 bytes (about 32 ms) is kept in the recording, but it is not forwarded to the live recognizer, so very small frames delay the text you see rather than losing audio.

### Keeping the connection alive

Send `{"type": "ping", "data": {}}` whenever you want to confirm the socket is healthy; the server replies with `{"type": "pong", "data": {}}`. Any inbound frame, audio included, counts as activity. A session that receives nothing for **15 minutes** is closed by the server, so send audio continuously, or ping during long pauses.

<Warning>
  One socket per session. Opening a second connection for the same session evicts the first: the older socket stops receiving transcripts. Reconnect after a drop, but never stream the same consultation from two places at once.
</Warning>

## Messages you receive

Every message is JSON with a `type` and a `data` field.

| Type               | Meaning                                                                                 |
| ------------------ | --------------------------------------------------------------------------------------- |
| `ready`            | The server is prepared to receive audio                                                 |
| `transcript`       | Text recognized so far. `data.is_final` marks a stable segment                          |
| `correction`       | A refined transcript segment that replaces earlier text for that segment                |
| `scribe_completed` | The audio was accepted and the record is being generated. `data.status` is `processing` |
| `scribe_error`     | Generation failed for this session                                                      |
| `reconnect`        | The server is asking you to reconnect. `data.reason` says why                           |
| `error`            | Something failed. Retry only when `data.retriable` is true                              |
| `pong`             | Answer to your `ping`                                                                   |

A `correction` supersedes what a `transcript` reported for the same segment. When you display text to a clinician, apply corrections as they arrive.

`reconnect` carries two reasons. `server_draining` means the server is restarting: reconnect and continue streaming. `resume_upload` means the stop arrived before all the audio did: reconnect and send the audio that is missing.

## Ending the session

Send this message when the consultation ends:

```json theme={null}
{ "type": "stop_recording", "data": {} }
```

The server finishes processing the audio it holds, sends `scribe_completed` with `data.status` set to `processing`, and closes the connection.

<Warning>
  `stop_recording` is the only thing that produces a record. If the socket simply closes without it, the audio is kept but nothing is generated, no webhook fires, and the consultation stays unfinished. Always send it, including when the clinician cancels.
</Warning>

<Tip>
  The socket does not deliver the medical record. `scribe_completed` means generation started, not that it finished. Wait for the `scribe_session.completed` webhook, then fetch the record.
</Tip>

## Receiving the record

The `scribe_session.completed` webhook carries `consultationInternalId`, the same `consultation.id` you sent. Fetch the record with it:

```bash theme={null}
curl https://scribe-api.telepatia.ai/v1/scribe-sessions/CONSULT-12345/medical-record-documents \
  -H "Authorization: Bearer YOUR_API_KEY"
```

Completion usually arrives within a minute of `stop_recording`. Generation runs partly while the audio streams, so a longer consultation does not take proportionally longer to finish.

<Warning>
  The webhook fires a few seconds before the document is queryable. If your first fetch returns an empty list, wait 5 seconds and fetch again.
</Warning>

## Connection loss

Reconnecting is expected and safe.

* **The socket drops while recording.** Call `POST /v1/scribe-sessions/live` again with the same `consultation.id`, then connect to the URL it returns. Audio already received is kept.
* **`live.url` reaches `expiresAt`.** Request a new session with the same `consultation.id`. A socket already open is unaffected.
* **Close 1013.** Retryable: the server is busy or restarting. Wait a moment and reconnect.
* **Close 1003.** The audio could not be decoded. The session moves to an error state, so reconnecting does not save it. Fix the encoder and start a new consultation.
* **Close 1008.** Terminal: the credential or the session is not valid. Do not reconnect; retrying returns the same result.

## Errors

| Status | Meaning                                                                                                                                                                |
| ------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `400`  | The body is invalid. `param` names the field                                                                                                                           |
| `401`  | The API key is missing or invalid                                                                                                                                      |
| `403`  | The API key lacks `scribe:write`, the `doctor` role, or an institution binding                                                                                         |
| `404`  | `param` `template.id`: the template does not exist. `param` `patient.id`: the patient does not exist. `param` `consultation.id`: this id is not available for your key |
| `409`  | `param` `consultation.id`: the id belongs to another consultation, or this one already stopped recording. `param` `template.id`: the template exists but is not active |
| `413`  | The JSON body is over 1 MiB. A large inline `template.sections` can reach it. `param` is `body`                                                                        |
| `503`  | Live sessions are unavailable. Retry with backoff                                                                                                                      |

See [Errors](/scribe-api/errors) for the full response envelope.
