# Endpointing

**Endpointing** decides when an utterance ends: after a pause of `endpointing` seconds, its final transcript is
sent. It sets the trade-off between fast finals and whole sentences.

| field | range | when omitted |
|---|---|---|
| `endpointing` | 0.01 to 10 s | 0.64 s |
| `maximum_duration_without_endpointing` | 5 to 60 s | no limit |

```json
{ "endpointing": 0.3, "maximum_duration_without_endpointing": 15 }
```

In a session, with the SDKs:

**Python SDK**

```python title="endpointing.py"
# pip install gladiaio-sdk
import os
import sys
import threading
import wave

from gladiaio_sdk import GladiaClient, LiveV2InitRequest

API_URL = os.environ.get("GLADIA_API_URL", "https://eu1.api.gladia.io")
API_KEY = os.environ["GLADIA_API_KEY"]

with wave.open(sys.argv[1]) as wav:  # a 16-bit PCM WAV file
    rate, channels = wav.getframerate(), wav.getnchannels()
    audio = wav.readframes(wav.getnframes())

config = {
    "encoding": "wav/pcm",
    "sample_rate": rate,
    "bit_depth": 16,
    "channels": channels,
    "model": "solaria-3",
    "language_config": {
        "languages": [
            "en"
        ]
    },
    "endpointing": 0.3,
    "maximum_duration_without_endpointing": 15
}

client = GladiaClient(api_key=API_KEY, api_url=API_URL)
session = client.live_v2().start_session(LiveV2InitRequest.from_dict(config))
ended, errors = threading.Event(), []

@session.on("message")
def on_message(message):
    if message.type == "transcript" and message.data.is_final:
        print(message.data.utterance.text)

session.on("error", errors.append)
session.once("ended", lambda _: ended.set())

step = rate * channels * 2 // 10  # 100 ms of audio per chunk
for start in range(0, len(audio), step):
    session.send_audio(audio[start : start + step])  # live audio: send each chunk as it is captured
session.stop_recording()

ended.wait(timeout=60)
sys.exit(1 if errors or not ended.is_set() else 0)
```

**JavaScript SDK**

```js title="endpointing.mjs"
// npm install @gladiaio/sdk
import { readFileSync } from "node:fs";
import { GladiaClient } from "@gladiaio/sdk";

const { sampleRate, channels, pcm } = readWav(process.argv[2]); // a 16-bit PCM WAV file

const config = {
  encoding: "wav/pcm",
  sample_rate: sampleRate,
  bit_depth: 16,
  channels,
  model: "solaria-3",
  language_config: {
    languages: [
      "en"
    ]
  },
  endpointing: 0.3,
  maximum_duration_without_endpointing: 15
};

const client = new GladiaClient({
  apiKey: process.env.GLADIA_API_KEY,
  apiUrl: process.env.GLADIA_API_URL ?? "https://eu1.api.gladia.io",
});
const session = client.liveV2().startSession(config);

session.on("message", (message) => {
  if (message.type === "transcript" && message.data.is_final) {
    const { text, end, forced } = message.data.utterance;
    console.log(`${end.toFixed(1)}s ${forced ? "(forced) " : ""}${text}`);
  }
});
session.on("error", (error) => {
  console.error(error);
  process.exitCode = 1;
});
session.once("ended", () => process.exit());

const step = (sampleRate * channels * 2) / 10; // 100 ms of audio per chunk
for (let start = 0; start < pcm.length; start += step) {
  session.sendAudio(pcm.subarray(start, start + step)); // live audio: send each chunk as it is captured
}
session.stopRecording();

function readWav(path) {
  const file = readFileSync(path);
  let fmt, pcm;
  for (let pos = 12; pos < file.length; ) {
    const id = file.toString("ascii", pos, pos + 4);
    const size = file.readUInt32LE(pos + 4);
    if (id === "fmt ") fmt = { channels: file.readUInt16LE(pos + 10), sampleRate: file.readUInt32LE(pos + 12) };
    if (id === "data") pcm = file.subarray(pos + 8, pos + 8 + size);
    pos += 8 + size + (size % 2);
  }
  return { ...fmt, pcm };
}
```

**HTTP**

Create the session, then stream its audio to the returned `url` ([Live WebSocket](/api-reference/v2/live/websocket/)):

```bash
curl -X POST https://eu1.api.gladia.io/v2/live \
  -H "x-gladia-key: $GLADIA_API_KEY" \
  -H "content-type: application/json" \
  -d '{
    "encoding": "wav/pcm",
    "sample_rate": 16000,
    "bit_depth": 16,
    "channels": 1,
    "model": "solaria-3",
    "language_config": {
      "languages": [
        "en"
      ]
    },
    "endpointing": 0.3,
    "maximum_duration_without_endpointing": 15
  }'
```

- A **short** `endpointing` (0.2 to 0.4 s) gives faster finals, and cuts sentences at short hesitations.
- A **long** one (0.8 s and more) gives whole sentences, later.
- `maximum_duration_without_endpointing` forces a final after that much speech without a pause; such an utterance
  carries `"forced": "max_duration"`.

[`GET /v2/live/defaults`](/api-reference/v2/live/defaults/) returns the values in force for your account.

For a voice agent, keep endpointing for the transcript and use [end-of-turn detection](/live/end-of-turn/) to
decide when to answer: a pause in the middle of a sentence is not the end of a turn.

## Limits and gotchas

- **Too short cuts words.** Under 0.2 s, a breath or a hesitation ends the utterance: finals split mid-sentence.
- **`maximum_duration_without_endpointing` cuts anywhere.** A forced final can end mid-word: keep it for
  monologues and noisy lines without pauses.
- **It is not end of turn.** A pause ends an utterance, not the speaker's turn: voice agents should answer on
  [end of turn](/live/end-of-turn/).
- **Python SDK**: an utterance's `forced` is not in the SDK's objects (gladiaio/sdk#61); the JavaScript SDK has it.

## Number hold

A phone number, an amount or an address dictated in pieces ("my card number is 4970… 1234… 5678") would be cut
into several finals, one per pause. The **number hold** keeps the utterance open while the number is still being
dictated, up to 2 seconds without new words, then closes it as usual.

It is **on by default** where the model supports it ([Models](/introduction/models/#features-by-model)); the
session is never refused for it. Turn it off with:

```json
{ "turn_router": false }
```
