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.

POST/api/v1/transcriptssecret key

Send 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.

ParameterInDescription
external_idrequiredbodyYour 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.
segmentsbodyExactly 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.
textbodyExactly 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.
formatbody"text" (default) or "vtt". Only with text.
titlebodyUp to 200 characters. Stored with personal names removed; company names stay.
started_atbodyISO 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_secbodyInteger, 0 or more.
languagebodyISO 639-1 code of the customer's speech, for example "de".
urlbodyhttps link back to the call in the source tool, up to 500 characters. Shown as the deep link on every signal.
owner_emailbodyThe 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_namebodyHow the rep is labelled in the transcript. A label equal to it (case-insensitive), or to its first name, maps to "me".
folderbodyUp to 100 characters. Your own taxonomy, kept as provenance.
attendeesbodyUp 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.

Request (segments)
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 }
    ]
  }'
Request (text)
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."
  }'
202 Accepted · queued
{
  "id": "01KX8VPHE0XN0JCDM6GSRC5HDQ",
  "status": "queued",
  "metered_items": 6,
  "word_count": 2740
}
202 Accepted · skipped
{
  "id": "01KX8VPHE0XN0JCDM6GSRC5HDQ",
  "status": "skipped",
  "skip_reason": "no_quotable_speech",
  "metered_items": 0,
  "word_count": 912
}
200 OK · external_id already recorded
{
  "id": "01KX8VPHE0XN0JCDM6GSRC5HDQ",
  "status": "done",
  "metered_items": 6,
  "word_count": 2740
}
400 Bad Request
{
  "type": "https://sentriment.com/problems/invalid-request",
  "title": "Request body failed validation",
  "status": 400,
  "detail": "body: provide exactly one of segments or text"
}
ParameterInDescription
202statusNew 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.
200statusexternal_id was already recorded in this project. { id, status, metered_items, word_count } for the existing row; nothing is counted again.
400 invalid-requeststatusValidation 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 unauthorizedstatusMissing, revoked, or a widget (pk_) key.
413 payload-too-largestatusBody over 2 MB.
429 rate-limitedstatusOver 60 per minute; Retry-After says how long.
500 internalstatusNothing 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.

GET/api/v1/transcripts/{id}secret key

Get 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.

ParameterInDescription
idrequiredpathThe id from the 202 or 200 response.
Request
curl https://app.sentriment.com/api/v1/transcripts/01KX8VPHE0XN0JCDM6GSRC5HDQ \
  -H "Authorization: Bearer sk_live_…"
200 OK
{
  "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"
}
ParameterInDescription
statusfieldqueued · extracting · done · skipped · failed · plan_blocked.
skip_reasonfieldPresent when status is skipped.
failure_reasonfieldPresent when status is failed. A code, for example provider_5xx or schema_refused, never a message.
signal_countfieldFeedback items extracted from this call. 0 until status is done.
extracted_atfieldPresent once extraction finished.
404 not-foundstatusNo such transcript in this project; another project's id is a 404, never a 403.
Was this page helpful?