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

# Transcribe recordings with timestamps

> Submit async Hear jobs, read word and segment timestamps, preserve the media timeline, receive signed webhooks, and understand retention and limits.

Use async Hear jobs when you need timestamps, speaker separation, SRT/VTT
subtitles, or processing that should continue after the request disconnects.
The synchronous OpenAI-compatible endpoint returns plain text. Timestamped
recordings use `POST /v1/transcription/jobs`.

<Info>
  Async jobs transcribe the same eight languages as Hear sync/streaming (`en`,
  `es`, `fr`, `de`, `hi`, `it`, `pt`, `nl`). The spoken language is auto-detected
  per call, and the transcript is returned in the detected language. The API does
  not currently return the detected language code. The optional job `language`
  field controls Recap summarization language only; it does not change
  transcription language. Hear sync/streaming has a separate explicit `language`
  parameter.
</Info>

## Submit a recording

Provide exactly one source:

* `audio_url`: PyAI fetches an HTTPS URL transiently and does not write the
  input bytes to durable input storage. Maximum download size: 512 MiB.
* Multipart `audio`: upload one file, up to 1 GiB. Uploaded input audio is
  retained for up to 7 days.

The input must contain a decodable audio stream. There is no separate media
duration ceiling. Request JSON, SRT, or VTT output. The JSON result is returned
inline unless it is large enough to require a signed `result_url`.

```bash theme={null}
curl https://api.pyai.com/v1/transcription/jobs \
  -H "Authorization: Bearer $PYAI_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: clip-481-v1" \
  -d '{
    "audio_url": "https://media.example.com/clip-481.wav",
    "channel": true,
    "vocabulary": ["CodePro", "Facility Coder Bundle", "Sachin Arora"],
    "output_formats": ["json", "srt", "vtt"],
    "webhook_url": "https://app.example.com/webhooks/pyai"
  }'
```

Or upload the audio directly:

```bash theme={null}
curl https://api.pyai.com/v1/transcription/jobs \
  -H "Authorization: Bearer $PYAI_API_KEY" \
  -F audio=@clip-481.wav \
  -F channel=true \
  -F 'vocabulary=["CodePro","Facility Coder Bundle","Sachin Arora"]' \
  -F output_formats=json,srt,vtt \
  -F webhook_url=https://app.example.com/webhooks/pyai
```

Multipart uploads are not deduplicated. Retry an ambiguous upload only after
checking whether the original job was created. Prefer `audio_url` plus
`Idempotency-Key` when safe automatic retries are required.

The submission returns `202` with `status: "queued"`. Poll
`GET /v1/transcription/jobs/{job_id}` until the status is `completed`, `failed`,
or `cancelled`.

<Note>
  Send `trace: true` to run a deterministic PII scan on the completed transcript.
  The organization must be entitled for Trace on transcription (otherwise
  `402`). Do not combine `trace` with `channel` or `diarize`; that combination
  returns `400`. See [Trace](/guides/trace-guardrails).
</Note>

## Bias known names and terms

Set `vocabulary` when a recording contains known names, brands, products, or
other distinctive terms. The list is optional and applies only to that job.
Omitting it preserves the normal transcription path unless your organization
has explicitly enabled stored vocabulary for the `batch` profile.

PyAI sanitizes the list before saving the job:

1. Surrounding whitespace is trimmed and repeated internal whitespace is
   collapsed.
2. Non-string entries and terms shorter than 4 characters are ignored.
3. Terms longer than 64 characters are ignored.
4. Phrases longer than 5 words are ignored.
5. Common-only terms are ignored.
6. Matching is case-insensitive for duplicates. The first spelling and order
   are preserved.
7. The first 5 valid terms are used. Extra terms are ignored.

Invalid vocabulary entries do not reject the job. If every entry is removed,
the job runs exactly as if `vocabulary` were omitted. The field does not choose
the transcription language. Vocabulary biases recognition toward the listed
terms; it does not steer the output script (for Hindi audio the transcript is
Devanagari regardless of how a term is spelled in the list, see
[language support](/reference/language-support)). Job responses keep their
existing shape and do not echo the vocabulary.

<Accordion title="Current common-word denylist">
  A phrase is removed only when every normalized word is in this list. A phrase
  such as `Acme Support` remains eligible because `Acme` is distinctive.

  ```text theme={null}
  account, address, agent, about, after, again, also, appointment, because,
  before, being, between, business, call, company, contact, could, customer,
  does, doing, email, from, have, hello, here, information, just, like, more,
  most, name, number, okay, order, other, over, payment, phone, please, product,
  same, service, should, some, support, team, than, thank, thanks, that, their,
  them, then, there, these, they, this, those, today, very, what, when, where,
  which, while, with, would, yeah, your
  ```
</Accordion>

### Stored organization vocabulary

Use `PUT /v1/hear/vocabulary` with the `hear:configure` scope to save up to five organization-owned terms.
Stored terms remain inactive until `enabled_for` includes `batch`. If a job also
sends `vocabulary`, those request terms come first and stored suggestions fill
remaining slots up to five. The effective list is saved with the job and stays
the same on every chunk, retry, and fallback.

Hear does not read a global glossary. It also does not automatically import
names from a CRM, dialer, or callee record. Automatic ingestion is a separate
follow-up.

## Timestamped result

This example shows the response shape and field units. The values are
illustrative:

```json theme={null}
{
  "job_id": "job_aZ09...",
  "status": "completed",
  "created_at": 1787220000000,
  "updated_at": 1787220012000,
  "result": {
    "text": "[speaker_1] Hello everyone.",
    "audio_seconds": 120,
    "speakers": 1,
    "words": [
      {
        "word": "Hello",
        "start": 0.42,
        "end": 0.81,
        "confidence": 0.98,
        "speaker": "speaker_1",
        "channel": 0
      }
    ],
    "segments": [
      {
        "id": 0,
        "text": "Hello everyone.",
        "start": 0.42,
        "end": 1.8,
        "speaker": "speaker_1",
        "channel": 0
      }
    ],
    "formats": {
      "srt": "https://storage.example/signed-result",
      "vtt": "https://storage.example/signed-result"
    }
  }
}
```

`start`, `end`, and `audio_seconds` are decimal seconds. Word and segment
offsets start at `0.0` on the decoded source-media timeline. Leading,
inter-word, and trailing silence are not removed or compacted. Resampling and
internal chunking do not shift later offsets.

`words[]` carries per-word times for `en`, `es`, `fr`, `de`, `hi`, `it`, and
`pt`. Hindi is native Devanagari. `nl` keeps turn-level `segments` with an
empty `words` array.

`confidence` is optional. Punctuation is attached to a word when formatting can
be aligned safely. Punctuation is not a separately timestamped token.
`dictation` and `drop_fillers` can change the number of words. If the rewritten
text cannot be safely realigned, the transcript remains available and `words`
is empty.

Segments are subtitle-ready spans built from the words. A segment starts when
the speaker/channel changes or after a readable pause, so segment boundaries
can change even when word timestamps do not.

## Speaker diarization and channel separation

For a complete mono-audio example, fallback handling, and the distinction between
speaker labels and identity, see the [speaker diarization guide](/guides/speaker-diarization).

* For stereo audio, use `channel: true`. Channel 0 is `speaker_1`, channel 1 is
  `speaker_2`. Separation is channel-based and exact, but labels are neutral:
  PyAI does not infer which channel is the agent or customer.
* For mono audio, use `diarize: true`. Speaker labels are model-derived and
  identify turns within that result. Do not use them as stable person IDs across
  separate jobs. If diarization cannot be produced for a recording, the job
  can still complete with a transcript and no speaker labels. Check whether
  returned segments contain `speaker` before relying on speaker attribution.
* Do not set both options. Prefer `channel` whenever each participant already
  occupies a separate channel.

## Idempotency, cancellation, and failures

`Idempotency-Key` applies to JSON `audio_url` submissions. Retrying the same key
and body replays the original `202`. Using the key with a different body returns
`409 idempotency_conflict`. Multipart uploads are not deduplicated.

`DELETE /v1/transcription/jobs/{id}` cancels queued or running work. It is
idempotent on terminal jobs, but it is not a data-erasure endpoint.

A failed job has `status: "failed"` and a normalized human-readable `error`.
That field is not a stable machine-readable failure code. Submission,
authorization, billing, and rate failures use the codes in
[Errors and limits](/errors-and-limits).

## Signed webhooks

When `webhook_url` is set, PyAI sends:

```json theme={null}
{
  "type": "transcription.job.completed",
  "created": 1787220012,
  "data": {
    "job_id": "job_aZ09...",
    "status": "completed"
  }
}
```

Failure deliveries use `transcription.job.failed`. Verify the exact request
body with `X-PyAI-Signature`:

```text theme={null}
t=<unix_seconds>,v1=<hex_hmac>
HMAC-SHA256(secret, "<t>.<rawBody>")
```

Reject stale timestamps and deduplicate retries using `X-PyAI-Event-Id` or the
delivery `Idempotency-Key`. Mint or rotate the organization webhook secret with
`POST /v1/webhooks/signing-secret`.

## Retention and data use

Async jobs are not a zero-retention surface:

* URL-fetched input bytes are not persisted.
* Uploaded input audio is retained for up to 7 days.
* Job result artifacts are retained for up to 30 days.
* Signed result links normally expire after 7 days.
* There is no `store: false` job option and cancellation does not erase data.

Customer Content remains yours. The [Terms of Service](https://pyai.com/legal/terms)
grant PyAI a limited license to process it solely to provide the Services. The
API does not expose a training opt-out flag. If you require contractual
zero-retention, a no-training warranty, a deletion SLA, a subprocessor schedule,
or backup-retention terms, confirm those requirements in a data-processing
agreement before sending production data.

PyAI currently processes data in one US region and does not offer India data
residency. See [Security and data handling](/security-and-data) and
[Reliability and regions](/reference/reliability).

## Pricing, rate limits, and versioning

Async Hear jobs report their metered usage in the response. For current rates,
included usage, and plan availability, see the
[pricing page](https://pyai.com/pricing). Your key's request rate, burst,
concurrency, and quota posture are returned by `GET /v1/me`.

There is no published job-completion timeout SLA. Processing time depends on
media length, queue load, diarization, and requested formats.

Every control-plane response includes `X-PyAI-Contract-Version`, which matches
the live OpenAPI `info.version`. The transcription result does not currently
echo a model version or a separate result-schema version.

<CardGroup cols={2}>
  <Card title="API reference" href="/api-reference/transcription-jobs/create-an-async-transcription-job">Exact request and response schemas.</Card>
  <Card title="Format Hear transcripts" href="/guides/hear-transcript-formatting">Punctuation, dictation, fillers, and timestamp realignment.</Card>
  <Card title="Security and data" href="/security-and-data">Retention windows, tenancy, and regions.</Card>
  <Card title="Errors and limits" href="/errors-and-limits">Stable request errors and retry guidance.</Card>
</CardGroup>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.