Plum / developers

Turns and contacts

Read transcript turns and control detected or turn-specific attribution.

Turns are the transcript text Optima publishes from confirmed audio. Contacts are the people you name in your account. This guide covers reading turns, playing their audio, managing contacts, and assigning turns to contacts.

For how speakers, contacts, and turns relate, see Data model.

Read turns

GET /turns returns current turns overlapping an absolute time window.

Query parameterRequiredDescription
fromYesAbsolute start of the window
toYesAbsolute end of the window. Must be later than from.
contact_idsNoComma-separated effective contact UUIDs, up to 100, without duplicates
source_idsNoComma-separated source UUIDs, up to 100, without duplicates
stream_idsNoComma-separated stream UUIDs, up to 100, without duplicates. Returns only turns of those recordings.
limitNoPage size. Defaults to 25; the maximum is 100.
cursorNoOpaque cursor from the previous page

Write from and to as ISO 8601 timestamps with an explicit Z or UTC offset, such as 2026-09-24T10:00:00Z or 2026-09-24T12:00:00.250+02:00. Optima supports up to three fractional-second digits (milliseconds). A timestamp without an offset, or with finer precision, returns 422. URL-encode the values in a query string; for example, + becomes %2B.

Filters combine with AND; the IDs inside one filter combine with OR. To read one recording's turns, pass its stream ID in stream_ids and a window that covers the recording, such as its captured_at through captured_at plus end_ms. Turns of a stream without a captured_at have no absolute time and are not returned by time windows.

To page, pass the returned cursor with the same window and filters. See API conventions.

GET /turns/{id} reads one current turn. A turn whose transcript was deleted or expired under the workspace's retention settings is left out of GET /turns, and reading it by ID returns 410 CONTENT_DELETED or CONTENT_EXPIRED with removed_at; see Removed content.

List items leave out content.words to keep pages small; their content has only schema_version and text. Read a turn with GET /turns/{id} for its word timings. Turn mutation responses also return the full turn.

Turn fields

FieldNullableDescription
idNoThe turn's stable ID
content.textNoThe transcript text. It can be empty when nothing was said; a turn whose transcript was removed is never returned.
content.wordsNoWord timings, returned by single-turn reads and mutations only; the array can be empty. Each word's start_ms and end_ms are milliseconds from the start of the turn.
source_idNoThe source that recorded the audio
stream_idNoThe stream (recording) the turn belongs to. Read it with GET /audio/streams/{id}, or list all of a recording's turns with GET /turns?stream_ids=.
speaker_idYesThe persistent detected voice. Turns from the same recognized speaker share this ID.
started_at, ended_atYesAbsolute start and end times. Both are null when the capture time is unknown.
clock_uncertainty_msYesUncertainty of the recording clock, in milliseconds
detected_contact_idYesThe contact of the turn's linked speaker
override_enabledNoWhether a turn-specific override applies
override_contact_idYesThe contact set by the override
contact_idYesThe effective contact after any override
contact_sourceYesdetected, override, or null
audioNo{ state, removed_at }: whether the turn can be played. state is available, or expired or deleted once audio of any chunk the turn spans was removed.

Speechmatics turns include provider word times. The optional hosted Parakeet path uses native decoder timing for words it can match to the published transcript and estimates timing for any remaining words within the segment. A hosted turn always has one timed word entry per transcript word. Word offsets are relative to the turn, including across capture chunks.

Accounts on the streaming speech engine receive turns about a minute at a time, with decoder timing for every word. A turn is one speaker's speech, grouped across short pauses:

  • A speaker's words stay in one turn across pauses of up to a second (two seconds when nobody else speaks). Someone else's interjection under 1.5 s, such as "yeah" or "right", stays inside the turn instead of splitting it.
  • Longer turns end more readily: after 30 s, anyone else speaking ends the turn, and after 60 s so does any pause of a quarter second. No turn is longer than 90 s.
  • When two people talk at once, each gets their own turn, so turns of different speakers can overlap in time. Order turns by started_at; do not assume one ends before the next starts.

A turn whose speaker is still talking at the end of a minute is published when the turn ends, with a later minute's turns, together with any turn that overlaps it, so the newest speech can arrive a minute or two after it was spoken. Optima publishes each turn once, in its final form, rather than publishing part of it and replacing it later. Each publication is a run.published event; refresh the source's turns after one. A recording's final turns are published when its stream ends; a recording that is never ended is finalized a few minutes after its audio stops (longer for devices), or soon after its source starts its next stream.

On the streaming engine a voice's first turns can arrive with a null speaker_id while Optima listens for enough of it (about ten seconds of speech) to compare it with voices it recognized before. Once Optima decides, it fills in speaker_id on those turns in place: the turn IDs, text, and any turn-specific contact override stay the same, and each such turn produces a turn.updated event. The voice's later turns arrive with the speaker_id directly. A voice Optima recognizes takes its known speaker_id; a new voice gets a new one, which later captures of the same voice can share. A voice heard more briefly receives its speaker_id when its recording session ends: with about five seconds of speech or more it is compared with known voices then; with less it gets a new speaker_id of its own, without a comparison.

After that, a turn's speaker_id changes when Optima finds that two detected speakers are the same voice and merges them: the newer speaker's turns move to the older speaker's speaker_id, and each of them produces a turn.updated event with speaker_id in changed_fields. When only one of the two speakers was assigned to a contact, the merged speaker keeps that contact, so detected_contact_id, and contact_id without an override, can change too; changed_fields then includes contact_id. Speakers assigned to different contacts are never merged.

Turn responses carry contact IDs, not embedded contact records. Resolve names through /contacts. A null speaker_id means the turn is not linked to a detected voice, or, on the streaming engine, not yet.

Play a turn's audio

  1. Call GET /turns/{id}/audio. It returns ordered clips, each with audio_id, start_ms, and end_ms.
  2. Read each clip's audio with GET /audio/{audio_id} and download its bytes from file.url, a signed storage link valid until file.url_expires_at. Request it without API headers; read the record again for a fresh link.
  3. Play the range from start_ms to end_ms in each file.

Clip bounds are milliseconds from the start of that audio file. Word timings use a different origin: they are relative to the start of the turn.

Audio stateResult
Confirmed audio covers the whole turn200 with ordered clips
No confirmed audio overlaps the turn200 with data.items set to []
Confirmed audio covers only part of the turn, or has a gap409 CONFLICT
The turn's audio.state is not available: retention or a deletion removed audio it spans410 CONTENT_EXPIRED or CONTENT_DELETED, with no clips

Check the turn's audio before offering playback: when it is not available, disable play and show that the recording expired or was deleted.

Manage contacts

Processing never creates contacts automatically. Create and name them yourself. Contacts belong to the transcripts layer: reading them requires turns:read, and creating, renaming, or deleting them requires turns:write.

OperationRequest
Create a contactPOST /contacts with { "name": "Alex" }. An optional UUID id is accepted.
List contactsGET /contacts
Read a contactGET /contacts/{id}
Rename a contactPATCH /contacts/{id} with { "name": "Alex Smith" }
Delete a contactDELETE /contacts/{id} returns 204

A name can be null. A non-null name is trimmed and must contain 1–200 characters.

Deleting a contact clears that contact's references:

  • Speakers remain.
  • Enabled turn overrides remain enabled with a null contact.

There is no merge operation.

Assign turns to contacts

A detected speaker can point to a contact. Change attribution with PATCH /turns/{id}:

IntentJSON body
Override this turn with a contact{ "contact_id": "<contact_uuid>", "scope": "turn" }
Hide attribution on this turn{ "contact_id": null, "scope": "turn" }
Assign the detected speaker across its turns{ "contact_id": "<contact_uuid>", "scope": "speaker" }
Clear the detected speaker's contact{ "contact_id": null, "scope": "speaker" }
Remove this turn's override{ "override_enabled": false }

For example:

curl -X PATCH https://api.getoptima.com/turns/TURN_ID \
  -H 'Authorization: Bearer YOUR_ACCESS_TOKEN' \
  -H 'X-API-Version: 1' \
  -H 'Content-Type: application/json' \
  -d '{"contact_id":"CONTACT_ID","scope":"turn"}'

Turn scope

Turn scope affects only that turn, even if the contact was just created.

A null contact_id in turn scope suppresses attribution on that turn. To restore the detected value, send { "override_enabled": false }.

Speaker scope

Speaker scope updates the linked speaker and clears the initiating turn's override. Other turns linked to that speaker follow the new detected contact unless they have their own overrides.

Speaker scope returns 409 when the turn has no linked speaker.

Rejected input

The body must match one of the shapes in the table. An empty string is not a valid contact_id, and computed fields such as detected_contact_id and contact_source cannot be patched directly.

Delete a turn

DELETE /turns/{id} returns 204 and tombstones the turn without deleting its audio. Deleting it again also returns 204, and a turn whose transcript expired can still be deleted. Afterward GET and PATCH /turns/{id} return 410 CONTENT_DELETED. Turns from a deleted source are also absent from account reads and cannot be edited.

Keep views current

A speaker-level change can update turns other than the one you patched, and a deleted turn disappears from account reads. After either change, refresh your visible time window from its first page. Retention removes transcripts and audio without a per-turn event: after a workspace.retention_applied event, refresh cached turns, and drop any turn that answers 410.

See the API reference for full schemas and errors.

Contact creation accepts the contact name. Optima assigns the contact id; supplying an id in a create request is rejected.

On this page