Transcripts
Send a call transcript for extraction and read back its status.
Note
A transcript is not a feedback item. It is read once, the customer's own words are extracted as verbatim quotes, and each quote becomes a feedback item with source call. The transcript itself is never stored. What is extracted, what is kept and how calls are counted is on the Sales calls page; this page is the contract.
/api/v1/transcriptssecret keySend a transcript
One call per request. Returns 202 as soon as the row is recorded and the call is counted; extraction runs asynchronously, so poll GET /api/v1/transcripts/{id} for the result. Secret keys only: a widget key gets 401. 60 requests per minute per key. Bodies up to 2 MB.
| Parameter | In | Description |
|---|---|---|
external_idrequired | body | Your stable id for the call, 1 to 200 characters, such as the notetaker's meeting id. The idempotency key: a repeat returns 200 with the existing row and counts nothing. |
segments | body | Exactly one of segments or text. An array of up to 5,000 { speaker, text, start_sec? }. speaker is "me", "them" or the label as written; text is one turn of speech, up to 20,000 characters; start_sec is seconds from the start of the call, 0 or more. |
text | body | Exactly one of segments or text. The whole transcript as one string, read into at most 5,000 segments. With format "text" (default), lines such as Them: …, Me: …, [00:14:32] Them: …, Alice: … are read and an unlabelled line continues the previous segment. With format "vtt", WebVTT with optional <v Name> voice tags. |
format | body | "text" (default) or "vtt". Only with text. |
title | body | Up to 200 characters. Stored with personal names removed; company names stay. |
started_at | body | ISO 8601 date-time. More than 48 hours before receipt makes the call history; missing counts as now, which is live. More than 24 hours in the future is refused. |
duration_sec | body | Integer, 0 or more. |
language | body | ISO 639-1 code of the customer's speech, for example "de". |
url | body | https link back to the call in the source tool, up to 500 characters. Shown as the deep link on every signal. |
owner_email | body | The person who ran the notetaker, the rep. Sets your own domain for the attendee filter; its local part also maps a named speaker to "me". |
owner_name | body | How the rep is labelled in the transcript. A label equal to it (case-insensitive), or to its first name, maps to "me". |
folder | body | Up to 100 characters. Your own taxonomy, kept as provenance. |
attendees | body | Up to 50 { email?, name?, external? }. external defaults to "the email domain differs from owner_email's domain". Names are used to redact the transcript and are then dropped. Without at least one external attendee the call is recorded as skipped. |
Speaker mapping: me and them are literal. Any other label equal (case-insensitive) to owner_name, to its first name, or to the local part of owner_email is "me"; every other label is "them". Text with no speaker labels at all gives every segment the speaker "unknown", and the call is recorded as skipped with reason no_quotable_speech: nothing is guessed.
curl -X POST https://app.sentriment.com/api/v1/transcripts \
-H "Authorization: Bearer sk_live_…" \
-H "Content-Type: application/json" \
-d '{
"external_id": "note_8f3a2c",
"title": "Acme discovery call",
"started_at": "2026-09-12T14:00:00Z",
"duration_sec": 2700,
"url": "https://notes.example-notetaker.com/n/8f3a2c",
"owner_email": "sam@yourcompany.example",
"owner_name": "Sam Okafor",
"folder": "Sales",
"language": "en",
"attendees": [
{ "email": "sam@yourcompany.example", "name": "Sam Okafor" },
{ "email": "jane@acme.example", "name": "Jane Doe" }
],
"segments": [
{ "speaker": "me", "text": "Thanks for making the time. What prompted the call?", "start_sec": 12 },
{ "speaker": "them", "text": "Exports. Anything above 50k rows times out and the team exports every Monday.", "start_sec": 20 },
{ "speaker": "Jane Doe", "text": "Pricing per seat is hard to justify for the read-only users.", "start_sec": 69 }
]
}'curl -X POST https://app.sentriment.com/api/v1/transcripts \
-H "Authorization: Bearer sk_live_…" \
-H "Content-Type: application/json" \
-d '{
"external_id": "note_8f3a2c",
"started_at": "2026-09-12T14:00:00Z",
"owner_email": "sam@yourcompany.example",
"attendees": [{ "email": "jane@acme.example" }],
"format": "text",
"text": "[00:00:12] Me: Thanks for making the time. What prompted the call?\n[00:00:20] Them: Exports. Anything above 50k rows times out."
}'{
"id": "01KX8VPHE0XN0JCDM6GSRC5HDQ",
"status": "queued",
"metered_items": 6,
"word_count": 2740
}{
"id": "01KX8VPHE0XN0JCDM6GSRC5HDQ",
"status": "skipped",
"skip_reason": "no_quotable_speech",
"metered_items": 0,
"word_count": 912
}{
"id": "01KX8VPHE0XN0JCDM6GSRC5HDQ",
"status": "done",
"metered_items": 6,
"word_count": 2740
}{
"type": "https://sentriment.com/problems/invalid-request",
"title": "Request body failed validation",
"status": 400,
"detail": "body: provide exactly one of segments or text"
}| Parameter | In | Description |
|---|---|---|
202 | status | New row. { id, status, metered_items, word_count, skip_reason? } with status queued, skipped or plan_blocked. metered_items is the count charged: one per 500 words, minimum 2. |
200 | status | external_id was already recorded in this project. { id, status, metered_items, word_count } for the existing row; nothing is counted again. |
400 invalid-request | status | Validation failed; detail names the field, or the line count when the text could not be read or read into more than 5,000 segments. |
401 unauthorized | status | Missing, revoked, or a widget (pk_) key. |
413 payload-too-large | status | Body over 2 MB. |
429 rate-limited | status | Over 60 per minute; Retry-After says how long. |
500 internal | status | Nothing was recorded; safe to retry with the same external_id. |
Skip reasons: too_short (under 800 words), too_long (over about three hours; a call is refused whole, never sliced), no_quotable_speech (no segment attributed to the customer), no_external_attendee and internal_call (the model read the call and judged it an internal one; set after the 202, so it is seen on GET). The first four are decided at the door before any reading and are not counted; internal_call is counted, because the reading was the cost.
/api/v1/transcripts/{id}secret keyGet one transcript
Status and counts for one call, scoped to the key's project. Poll it after a 202 to learn how many signals were found. No text is returned, because none is kept.
| Parameter | In | Description |
|---|---|---|
idrequired | path | The id from the 202 or 200 response. |
curl https://app.sentriment.com/api/v1/transcripts/01KX8VPHE0XN0JCDM6GSRC5HDQ \
-H "Authorization: Bearer sk_live_…"{
"id": "01KX8VPHE0XN0JCDM6GSRC5HDQ",
"external_id": "note_8f3a2c",
"provider": "api",
"status": "done",
"metered_items": 6,
"word_count": 2740,
"signal_count": 9,
"created_at": "2026-09-12T15:02:11.000Z",
"extracted_at": "2026-09-12T15:03:40.000Z"
}| Parameter | In | Description |
|---|---|---|
status | field | queued · extracting · done · skipped · failed · plan_blocked. |
skip_reason | field | Present when status is skipped. |
failure_reason | field | Present when status is failed. A code, for example provider_5xx or schema_refused, never a message. |
signal_count | field | Feedback items extracted from this call. 0 until status is done. |
extracted_at | field | Present once extraction finished. |
404 not-found | status | No such transcript in this project; another project's id is a 404, never a 403. |