Transcribe recordings

The transcription endpoint turns a recording of Thai or English speech into a verbatim transcript, with word timestamps on request. Billing is by recording length, charged before transcription and refunded automatically on failure.

View as Markdown

POST /v1/stt reads a recording and returns what was said. Send the file as base64 in audio with the model id paxa-stt-lite-v1-preview. The transcript comes back verbatim, in the language spoken: a Thai sentence that switches into English mid-way is written as spoken. Nothing is translated. Like every endpoint, the request is charged before inference and refunded automatically when inference fails (Credits).

Note

paxa-stt-lite-v1-preview is a research preview. Requests bill at the published rate. The transcript quality and the option set may change between releases, and the changelog on the product page records each change.

# Encode without line wrapping: wrapped base64 breaks the JSON string.
AUDIO=$(base64 < meeting.m4a | tr -d '\n')
# --max-time covers an hour-long recording; curl defaults to no limit.
curl -X POST https://api.paxalabs.com/v1/stt \
  --max-time 300 \
  -H "Authorization: Bearer $PAXA_API_KEY" \
  -H "Content-Type: application/json" \
  -d "{\"audio\": \"$AUDIO\", \"model\": \"paxa-stt-lite-v1-preview\"}"
Response
{
  "text": "สวัสดีค่ะคุณสมชายยอดชำระของท่านจำนวน พันสองร้อยห้าสิบ บาทครบกำหนดวันที่ สิบห้า สิงหาคมนี้ชำระผ่านแอปได้ตลอด ยี่สิบสี่ ชั่วโมงค่ะ",
  "usage": {
    "seconds": 10.25,
    "credits": 1.43
  }
}

Billing#

A recording costs 8.33 credits per minute of audio, 500 credits per hour, billed by its exact length. The length is read from the file itself before the charge. The cost is known before any listening starts. Every request costs at least 0.1 credits. Word timestamps, speaker turns, the style, the language hint, the convention, and the vocabulary cost nothing extra.

The charge is reported only in the body's usage.credits, beside usage.seconds, the length that was billed. This endpoint sends no x-credits-charged header. A fast response and a slow one carry the same wire surface. When transcription fails after the charge, the refund is automatic and the response is 502 provider_error. A recording with no speech is a delivered empty transcript and is billed for its length.

Recordings and formats#

audio carries the raw file bytes as base64. Seven formats are read: MP3, WAV (PCM), FLAC, Ogg (Opus or Vorbis), M4A, AAC (ADTS), and WebM. The format and the length are read from the bytes. No declared field is consulted. A file that cannot be read as one of the seven answers 400 audio_invalid. A file whose length cannot be read from its container, such as a FLAC written without a sample count, answers the same code. None of this charges.

Base64 grows a payload by about a third, and a compressed file travels faster: Opus at 24 kbps or MP3 at 64 kbps carries an hour of speech under the size limit. A recording captured in the browser with MediaRecorder is a WebM file. Its length is read from its blocks, and it needs no re-encoding.

Length and size limits#

One request transcribes at most 60 minutes, and a longer recording answers 400 audio_too_long. The decoded file may be at most 25 MiB, and a larger one answers 413 audio_too_large. Neither refusal charges anything. Split longer recordings at a pause and send the parts as separate requests.

Speakers have a shorter ceiling of their own: a recording of 9 minutes or longer with diarization set to true answers 400 diarization_audio_too_long, uncharged. The same recording transcribes normally with the field off.

Language#

language tells the model what to expect, as a BCP 47 tag such as "th" or "en". The hint filters nothing. Speech in any other language is still transcribed as spoken. A Thai recording with English words and phrases inside it needs no setting. Omitted, the model detects the language itself. Set the tag when detection has picked the wrong language for a recording.

Word timestamps#

Set timestamps to "word" and the response carries a words array: one span per recognized word with its text and its start and end in seconds on the recording's own clock. Thai words are segmented for you, and Latin words and numerals arrive whole. It is the same span shape the speech API returns. A caption file built from one round-trips through the other. Thai writes no spaces between words. The spans are the reliable way to place a subtitle boundary. A recording with no speech carries an empty array.

curl
AUDIO=$(base64 < meeting.m4a | tr -d '\n')
curl -X POST https://api.paxalabs.com/v1/stt \
  --max-time 300 \
  -H "Authorization: Bearer $PAXA_API_KEY" \
  -H "Content-Type: application/json" \
  -d "{\"audio\": \"$AUDIO\", \"model\": \"paxa-stt-lite-v1-preview\", \"timestamps\": \"word\"}"
Response
{
  "text": "สวัสดีค่ะคุณสมชายยอดชำระของท่านจำนวน พันสองร้อยห้าสิบ บาทครบกำหนดวันที่ สิบห้า สิงหาคมนี้ชำระผ่านแอปได้ตลอด ยี่สิบสี่ ชั่วโมงค่ะ",
  "words": [
    {
      "text": "สวัสดี",
      "start": 0.12,
      "end": 0.52
    },
    {
      "text": "ค่ะ",
      "start": 0.6,
      "end": 0.739
    },
    {
      "text": "คุณ",
      "start": 1.64,
      "end": 1.8
    },
    {
      "text": "สมชาย",
      "start": 1.84,
      "end": 2.24
    },
    {
      "text": "ยอด",
      "start": 2.4,
      "end": 2.56
    },
    {
      "text": "ชำระ",
      "start": 2.56,
      "end": 2.839
    },
    {
      "text": "ของ",
      "start": 2.84,
      "end": 2.96
    },
    {
      "text": "ท่าน",
      "start": 2.96,
      "end": 3.14
    },
    {
      "text": "จำนวน",
      "start": 3.16,
      "end": 3.44
    },
    {
      "text": "พันสองร้อยห้าสิบ",
      "start": 3.52,
      "end": 4.56
    },
    {
      "text": "บาท",
      "start": 4.68,
      "end": 4.84
    },
    {
      "text": "ครบ",
      "start": 5,
      "end": 5.2
    },
    {
      "text": "กำหนด",
      "start": 5.24,
      "end": 5.48
    },
    {
      "text": "วัน",
      "start": 5.52,
      "end": 5.639
    },
    {
      "text": "ที่",
      "start": 5.64,
      "end": 5.719
    },
    {
      "text": "สิบห้า",
      "start": 5.84,
      "end": 6.08
    },
    {
      "text": "สิงหาคม",
      "start": 6.68,
      "end": 7.16
    },
    {
      "text": "นี้",
      "start": 7.2,
      "end": 7.34
    },
    {
      "text": "ชำระ",
      "start": 7.8,
      "end": 8.08
    },
    {
      "text": "ผ่าน",
      "start": 8.12,
      "end": 8.28
    },
    {
      "text": "แอป",
      "start": 8.28,
      "end": 8.44
    },
    {
      "text": "ได้",
      "start": 8.48,
      "end": 8.62
    },
    {
      "text": "ตลอด",
      "start": 8.64,
      "end": 8.88
    },
    {
      "text": "ยี่สิบสี่",
      "start": 9.04,
      "end": 9.439
    },
    {
      "text": "ชั่วโมง",
      "start": 9.56,
      "end": 9.88
    },
    {
      "text": "ค่ะ",
      "start": 9.92,
      "end": 10.06
    }
  ],
  "usage": {
    "seconds": 10.25,
    "credits": 1.43
  }
}

Spoken or written#

convention selects how the transcript writes what was said, and defaults to "spoken". The words are the same under both. What changes is numbers, units, dates, and repeated words. The recording behind the samples on this page says an amount, a date, and a duration:

What was saidspoken (default)written
1,250 baht, said in Thaiพันสองร้อยห้าสิบบาท1,250บาท
the 15th of Augustสิบห้าสิงหาคม15สิงหาคม
24 hoursยี่สิบสี่ชั่วโมง24ชั่วโมง

The whole transcript under "spoken" reads สวัสดีค่ะคุณสมชายยอดชำระของท่านจำนวน พันสองร้อยห้าสิบ บาทครบกำหนดวันที่ สิบห้า สิงหาคมนี้ชำระผ่านแอปได้ตลอด ยี่สิบสี่ ชั่วโมงค่ะ. Under "written" it reads สวัสดีค่ะคุณสมชายยอดชำระของท่านจำนวน 1,250 บาทครบกำหนดวันที่ 15 สิงหาคมนี้ชำระผ่านแอปได้ตลอด 24 ชั่วโมงค่ะ.

"spoken" writes numbers as they were pronounced and a word said twice as two words. Use it for a verbatim record, a subtitle that must match the audio, or text a language model will read, since the spoken form carries exactly what the speaker said. "written" keeps display conventions: digits and units, the repetition mark ๆ. Use it for dictated text that will be read on a page, such as a memo or a message. Word timestamps are identical under both.

curl
AUDIO=$(base64 < meeting.m4a | tr -d '\n')
curl -X POST https://api.paxalabs.com/v1/stt \
  --max-time 300 \
  -H "Authorization: Bearer $PAXA_API_KEY" \
  -H "Content-Type: application/json" \
  -d "{\"audio\": \"$AUDIO\", \"model\": \"paxa-stt-lite-v1-preview\", \"convention\": \"written\"}"
Response
{
  "text": "สวัสดีค่ะคุณสมชายยอดชำระของท่านจำนวน 1,250 บาทครบกำหนดวันที่ 15 สิงหาคมนี้ชำระผ่านแอปได้ตลอด 24 ชั่วโมงค่ะ",
  "usage": {
    "seconds": 10.25,
    "credits": 1.43
  }
}

Verbatim or clean#

style selects how closely the transcript follows the recording, and defaults to "verbatim": fillers, false starts, and self-corrections are written as they were said. "clean" drops the fillers and merges broken phrases into one. It is a readability choice. A clean transcript departs further from what was said, and our own measurement found it less exact. Pick it for a transcript a reader will skim. A record of what was said keeps the default. Any other value answers 400 validation. The convention applies on top of either style.

Speakers#

Set diarization to true and the response carries a segments array: one entry per speaker turn with its speaker number, its text, and its start and end in seconds. Speakers are numbered from 0 in order of first appearance in the recording. Nobody is identified. A turn's text is a substring of the transcript. The two never disagree. With timestamps set to "word" as well, every word span names its speaker. A diarized recording is read in one pass. Speaker numbers hold across the whole file. That pass is also what limits it. A diarized recording must be under 9 minutes, and one that long or longer answers 400 diarization_audio_too_long before any charge. It also takes longer to return than an undiarized one. Split a longer recording at a pause and read the speaker numbers per part. Each part numbers its speakers from 0. The recording behind the samples on this page has one voice. It comes back as one turn:

curl
AUDIO=$(base64 < meeting.m4a | tr -d '\n')
curl -X POST https://api.paxalabs.com/v1/stt \
  --max-time 300 \
  -H "Authorization: Bearer $PAXA_API_KEY" \
  -H "Content-Type: application/json" \
  -d "{\"audio\": \"$AUDIO\", \"model\": \"paxa-stt-lite-v1-preview\", \"diarization\": true}"
Response
{
  "text": "สวัสดีค่ะคุณสมชายยอดชำระของท่านจำนวน พันสองร้อยห้าสิบ บาทครบกำหนดวันที่ สิบห้า สิงหาคมนี้ชำระผ่านแอปได้ตลอด ยี่สิบสี่ ชั่วโมงค่ะ",
  "segments": [
    {
      "speaker": 0,
      "text": "สวัสดีค่ะคุณสมชายยอดชำระของท่านจำนวน พันสองร้อยห้าสิบ บาทครบกำหนดวันที่ สิบห้า สิงหาคมนี้ชำระผ่านแอปได้ตลอด ยี่สิบสี่ ชั่วโมงค่ะ",
      "start": 0.12,
      "end": 10.06
    }
  ],
  "usage": {
    "seconds": 10.25,
    "credits": 1.43
  }
}

Vocabulary#

vocabulary lists terms the recording is likely to contain, spelled the way they should appear in the transcript. Product names, people, places, and jargon qualify, in Thai or English. The list holds up to 50 terms of 50 characters each, and a longer list or term answers 400 validation. The terms bias recognition and are never inserted. A term that was not said does not appear. It is the lever for domain content: a meeting about your own product transcribes the product's name the way you spell it. A vocabulary combines with every other option.

curl
AUDIO=$(base64 < meeting.m4a | tr -d '\n')
curl -X POST https://api.paxalabs.com/v1/stt \
  --max-time 300 \
  -H "Authorization: Bearer $PAXA_API_KEY" \
  -H "Content-Type: application/json" \
  -d "{\"audio\": \"$AUDIO\", \"model\": \"paxa-stt-lite-v1-preview\", \"language\": \"th\", \"vocabulary\": [\"Paxa Labs\", \"quarterly review\"]}"

Reliability and retries#

The response is one buffered JSON body. A ten-second clip returns in a few seconds. An hour-long recording can take a minute or two. The recording is read in parallel pieces cut at pauses, and the wait grows slowly with the length. Set the client timeout generously. The samples above allow 300 seconds.

A request that runs long enough writes its response headers before the outcome is known. Intermediate proxies abandon a connection that has been silent too long, and the early headers prevent that. Such a response always has status 200. It reports a failure in the body, as the same problem document a failed request returns: a title and a status where text would be. Long recordings are the requests that reach it. Treat a body carrying title as the error it describes. Shorter recordings always carry their status on the status line.

Retries are safe with an Idempotency-Key. A replay of a delivered request serves the same transcript again without a second charge. The key covers the recording and every option. A replay with a different convention, style, or vocabulary is refused. When transcription fails, the charge is refunded and a fresh request needs a new key.