# Get word-level timestamps

Timestamps are word, sentence, or utterance timing spans returned together with the audio, for captions and read-along UIs. One request option changes the response shape and the rate.

Timestamps are timing spans, word, sentence, or utterance, returned together with the audio. Set `timestamps` on [POST /v1/tts](https://paxalabs.com/docs/api/text-to-speech) to receive them. The response then carries the audio together with timing spans, each holding the span's text, its start, and its end in seconds. Timed requests bill at 1.25 times the model's per-character rate, rounded up to the next hundredth of a credit, minimum 0.1 credit per request.

curl:

```bash
curl -X POST https://api.paxalabs.com/v1/tts \
  -H "Authorization: Bearer $PAXA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "text": "สวัสดีครับ ยินดีต้อนรับสู่ Paxa Labs",
    "voice": "khanomkrok",
    "model": "paxa-tts-flash-v2",
    "timestamps": "word"
  }' \
  -o response.json

jq -r .audio response.json | base64 -d > speech.mp3
jq .timing.spans response.json
```

TypeScript:

```typescript
import { writeFile } from "node:fs/promises";

const response = await fetch("https://api.paxalabs.com/v1/tts", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.PAXA_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    text: "สวัสดีครับ ยินดีต้อนรับสู่ Paxa Labs",
    voice: "khanomkrok",
    model: "paxa-tts-flash-v2",
    timestamps: "word",
  }),
});

if (!response.ok) throw new Error(`TTS failed: ${response.status}`);
const { audio, timing } = await response.json();

await writeFile("speech.mp3", Buffer.from(audio, "base64"));
for (const span of timing.spans) {
  console.log(`${span.start.toFixed(2)}s ${span.text}`);
}
```

Python:

```python
import base64
import os
import requests

response = requests.post(
    "https://api.paxalabs.com/v1/tts",
    headers={"Authorization": f"Bearer {os.environ['PAXA_API_KEY']}"},
    json={
        "text": "สวัสดีครับ ยินดีต้อนรับสู่ Paxa Labs",
        "voice": "khanomkrok",
        "model": "paxa-tts-flash-v2",
        "timestamps": "word",
    },
)
response.raise_for_status()
data = response.json()

with open("speech.mp3", "wb") as f:
    f.write(base64.b64decode(data["audio"]))

for span in data["timing"]["spans"]:
    print(f"{span['start']:.2f}s {span['text']}")
```

## Choose a granularity

The `timestamps` value picks how the audio is segmented; every span carries `text`, `start`, and `end` in seconds. Spans cover the spoken text, and an [emotion tag](https://paxalabs.com/docs/emotion-tags) takes no span.

| Value | Spans |
| --- | --- |
| word | One span per spoken word. The model segments Thai itself, and the text needs no spaces. |
| sentence | One span per sentence. |
| utterance | One span per continuous run of speech between pauses. An utterance can cover several sentences read in one breath, or part of a long one. |

## Response format

With buffered delivery the response is `application/json`, and the body is no longer binary audio. The `audio` field carries the complete file as base64. The `timing.spans` field lists every span at the requested granularity. Decoding `audio` yields exactly the bytes an untimed request returns, and the usual headers, including `x-credits-charged`, are unchanged.

Response:

```json
{
  "audio": "SUQzBAAAAAAAI1RTU0UAAAAPAAADTGF2ZjYy...",
  "timing": {
    "granularity": "word",
    "spans": [
      {
        "text": "สวัสดี",
        "start": 0,
        "end": 0.51
      },
      {
        "text": "ครับ",
        "start": 0.51,
        "end": 0.85
      },
      {
        "text": "ยินดี",
        "start": 0.85,
        "end": 1.27
      },
      {
        "text": "ต้อนรับ",
        "start": 1.27,
        "end": 1.87
      },
      {
        "text": "สู่",
        "start": 1.87,
        "end": 2.12
      },
      {
        "text": "Paxa",
        "start": 2.12,
        "end": 2.46
      },
      {
        "text": "Labs",
        "start": 2.46,
        "end": 2.8
      }
    ]
  }
}
```

## Stream timed audio

With `stream: true` the response is `application/x-ndjson`. Each line is one JSON event carrying a base64 audio chunk and the spans that chunk completed. A final `done` event carries the total span count. Compare it with the spans you collected. The spans themselves travel exactly once, on the chunk lines. If the stream ends without a `done` event, treat the request as failed. The charge is refunded automatically, under the same rule as [audio streaming](https://paxalabs.com/docs/streaming#failure-semantics).

NDJSON stream:

```json
{"type":"chunk","audio":"SUQzBAAAAAAAI1RTU0UAAAAPAAADTGF2ZjYy...","spans":[{"text":"สวัสดี","start":0,"end":0.51},{"text":"ครับ","start":0.51,"end":0.85},{"text":"ยินดี","start":0.85,"end":1.27}]}
{"type":"chunk","audio":"SUQzBAAAAAAAI1RTU0UAAAAPAAADTGF2ZjYy...","spans":[{"text":"ต้อนรับ","start":1.27,"end":1.87},{"text":"สู่","start":1.87,"end":2.12},{"text":"Paxa","start":2.12,"end":2.46},{"text":"Labs","start":2.46,"end":2.8}]}
{"type":"done","granularity":"word","span_count":7}
```

## Pricing

The surcharge applies only when `timestamps` is set. The same request then bills 1.25 times its untimed cost. The 36-character example above costs 0.45 credits timed and 0.36 credits untimed. [GET /v1/models](https://paxalabs.com/docs/api/models) lists each model's per-character rate. Rounding and the 0.1 credit minimum work as described in [Credits and billing](https://paxalabs.com/docs/credits). Try it on your own text in the [playground](https://paxalabs.com/app/speech). The model behind timed speech has its own page, [text to speech](https://paxalabs.com/text-to-speech).

> The [OpenAI-compatible endpoint](https://paxalabs.com/docs/api/speech) ignores `timestamps`, since the OpenAI request shape has no equivalent field. Timing is also available on every [live connection](https://paxalabs.com/docs/live-streaming), priced the same way.
