POSThttps://core-api.heysadie.ai/turn-flags
callId messageIndex expectedText errorTags comment bankId success data flag id organizationId tenantId assistantId kind heardText audio startSeconds endSeconds evaluation controlForTurnId fullCallRunId tieBreak transcriberId reason label review calibration createdBy createdAt updatedAt liveTranscriber name evaluationProgress stage done total controls metadata timestamp version path error code message details field existingFlagId limitFlag a caller line
Flag a caller line the call's live transcriber got wrong, with what the caller actually said. - The line's production attempt is the live transcriber's stored text, scored against `expectedText` at no cost. - With `bankId`, the flag joins that bank, the bank samples `controlLinesPerFlag` unflagged caller lines of the same call as controls, and the bank's candidates are queued on the flag and its controls. The returned turns stay `pending` until the candidates report: poll GET /turn-flags?callId=… or the bank's turns until `evaluation` leaves `pending`, showing `evaluationProgress`. A bank with no live candidate scores them at once. - Without `bankId`, the flag waits in no bank, `pending`, with no controls and nothing queued. Move it into a bank later with PATCH /turn-flags/:id; controls are sampled only for flags created in a bank. - A control sampled earlier on the same line is replaced by the flag. - No `Idempotency-Key` is honored here. A retried create gets 409 TURN_FLAG_EXISTS naming the flag the first attempt made, or 429 BACKLOG_FULL if the backlog filled meanwhile. Errors: 404 RESOURCE_NOT_FOUND when the call or `bankId` is not in the organization. 409 TURN_FLAG_EXISTS when the line already has a flag (`details.existingFlagId`). 422 LINE_NOT_FLAGGABLE when the line cannot be flagged (`details.reason`). 429 BACKLOG_FULL when the bank has candidates to queue and the organization already has more than 50 jobs waiting; nothing is written.
Try it in the API ExplorerExample request
curl -X POST "https://core-api.heysadie.ai/turn-flags" \
-H "Authorization: ApiKey YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"callId": "0199a1b2-7c3d-7e4f-8a5b-6c7d8e9f0a1b",
"messageIndex": 4,
"expectedText": "A table for four at 7:30 under Siobhan",
"errorTags": [
"name",
"time"
],
"comment": "Surname misheard",
"bankId": "0199a1b2-1111-7222-8333-944455556666"
}'Authentication
Authenticate with an API key in the Authorization header using the ApiKey scheme. See Authentication for how keys work and where to get them.
AuthorizationstringrequiredAPI key in the ApiKey scheme: ApiKey YOUR_API_KEY. Tenant keys authorize their own tenant; organization keys can act on any tenant in the organization.
X-Tenant-Idstring (uuid)optionalRequired when authenticating with an organization key: it selects the tenant the request acts on. Omit with tenant keys.
Request
Body application/json · required
callIdstringrequiredA call of the organization
messageIndexstring (integer) | integerrequiredIndex of a flaggable caller line, from GET /turn-flags/calls/:callId/lines
expectedTextstringrequiredWhat the caller actually said, word for word. 1 to 2000 characters, trimmed. Scoring ignores case and punctuation, but digits and number words stay distinct ("830" is not "eight thirty")
errorTagsstring[]requiredWhat went wrong. Each `^[a-z_]{1,32}$`, at most 8 after duplicates collapse. GET /turn-flags/tags suggests some; any matching tag is accepted
commentstring | nulloptionalA free note, up to 2000 characters. Blank or null clears it
bankIdstring | nulloptionalA live bank of the organization for the flag and its controls to join; its candidates are queued on them. Null or absent leaves the flag in no bank, with no controls and nothing queued; PATCH /turn-flags/:id moves it later
Responses
successbooleanrequiredtrue on every 2xx
dataobjectoptionalThe result
Show 2 child properties
flagobjectrequiredThe new flag
Show 25 child properties
idstringrequiredTurn id
organizationIdstringrequiredThe organization that owns the turn
tenantIdstringrequiredThe tenant the call belongs to
callIdstringrequiredThe call the line is from
assistantIdstring | nullrequiredThe assistant that took the call. Null when the call names none
messageIndexstring (integer) | integerrequiredIndex of the line in the call's messages, as GET /turn-flags/calls/:callId/lines lists it
bankIdstring | nullrequiredThe bank the turn belongs to. Null while it is in no bank, and then no candidate replays it
kindenumrequiredflagged: a person flagged the line as wrong and typed what was said. control: an unflagged caller line of the same call, sampled with a flag, whose expected text is the live transcriber's own. full_call: a caller line replayed by a full-call run, listed under that run
"flagged""control""full_call"heardTextstringrequiredWhat the live transcriber wrote for the line on the call, frozen when the turn was created
expectedTextstring | nullrequiredWhat the caller said, word for word: typed for a flag, the heard text for a control. A full-call turn carries the expected text of a flag on the same line, else null
errorTagsstring[]requiredTags on what went wrong, e.g. `name` or `spelling`. Empty on controls and full-call turns
commentstring | nullrequiredThe flagger's note. Null when none
audioobjectrequiredThe line's place in the call recording. Transcribers hear it with 0.75 s more on each side, so play `startSeconds - 0.75` to `endSeconds + 0.75`, clamped to the recording, to hear what they heard
Show 2 child properties
startSecondsnumberrequiredWhere the line starts, in seconds from the recording start
endSecondsnumberrequiredWhere the line ends, in seconds from the recording start
evaluationenumrequiredpending: candidate attempts are queued and not all reported, or the turn was created in no bank. scored: every transcriber its job asked for returned an attempt (a failed attempt counts), or it joined a bank that owed it no attempt. failed: its job ended without an attempt from every transcriber it asked for. A turn taken out of a bank keeps its evaluation, and a backfill does not move a scored turn back to pending: watch `evaluationProgress` for that work
"pending""scored""failed"controlForTurnIdstring | nullrequiredOn a control, the flag it was sampled for. Deleting or moving that flag deletes or moves the control. Null otherwise
fullCallRunIdstring | nullrequiredOn a full-call turn, its run. Null otherwise
tieBreakobject | nullrequiredWhen two or more candidates match the expected text with different raw text (casing, punctuation), the one an LLM judge found fits the venue best. Null otherwise
Show 2 child properties
transcriberIdstringrequiredThe candidate whose text the judge picked
reasonstringrequiredThe judge's one-sentence reason
labelenum | nullrequiredOn a full-call turn where the candidate differs from production, its label: decided by the expected-text matches where the line has expected text, else by the LLM judge. candidate_right: the candidate has what the caller said and production does not. production_right: production has it and the candidate does not. same_meaning: they differ in a way that does not matter. both_wrong: neither has it. Null otherwise or until the analysis labels it
"candidate_right""production_right""same_meaning""both_wrong"reviewenum | nullrequiredOn a full-call turn, a person's verdict from POST /full-call-runs/:id/reviews, in the same values as `label`. It counts only on differing turns, and outranks the label in `fixed` and `introduced` only where the line has no expected text. Null until reviewed
"candidate_right""production_right""same_meaning""both_wrong"calibrationbooleanrequiredOn a full-call turn, whether production is also replayed on it to measure replay against the live text. False otherwise
createdBystring | nullrequiredWho made it, as the creating request named them in an `X-On-Behalf-Of` header. Null when it sent none
createdAtstring (date-time) | string (date) | number | nullrequiredWhen it was created, UTC
updatedAtstring (date-time) | string (date) | number | nullrequiredWhen it last changed, UTC
liveTranscriberobject | nullrequiredThe transcriber the turn's call ran on, recorded when the turn was created. Its stored text is the turn's production attempt. Null when unknown
Show 2 child properties
idstringrequiredTranscriber id, as GET /transcribers lists it
namestringrequiredThe transcriber's display name
evaluationProgressobject | nullrequiredThe newest open transcribe job holding the turn, for a progress bar. Null when no open job holds it (scored, failed or never queued), and on the POST /turn-flags response, which is written before its job is queued: read the turn again to see it
Show 3 child properties
stageenumrequiredwaiting: the job is queued or dispatched and no worker has started it. scoring: a worker is running it
"waiting""scoring"donestring (integer) | integerrequiredTranscriptions the job has finished, over every clip and transcriber it holds, not this turn's alone. 0 until the worker first reports
totalstring (integer) | integerrequiredTranscriptions the job holds in all
controlsobject[]requiredThe control turns sampled with it, by line. Empty without a bank
Show 25 item properties
idstringrequiredTurn id
organizationIdstringrequiredThe organization that owns the turn
tenantIdstringrequiredThe tenant the call belongs to
callIdstringrequiredThe call the line is from
assistantIdstring | nullrequiredThe assistant that took the call. Null when the call names none
messageIndexstring (integer) | integerrequiredIndex of the line in the call's messages, as GET /turn-flags/calls/:callId/lines lists it
bankIdstring | nullrequiredThe bank the turn belongs to. Null while it is in no bank, and then no candidate replays it
kindenumrequiredflagged: a person flagged the line as wrong and typed what was said. control: an unflagged caller line of the same call, sampled with a flag, whose expected text is the live transcriber's own. full_call: a caller line replayed by a full-call run, listed under that run
"flagged""control""full_call"heardTextstringrequiredWhat the live transcriber wrote for the line on the call, frozen when the turn was created
expectedTextstring | nullrequiredWhat the caller said, word for word: typed for a flag, the heard text for a control. A full-call turn carries the expected text of a flag on the same line, else null
errorTagsstring[]requiredTags on what went wrong, e.g. `name` or `spelling`. Empty on controls and full-call turns
commentstring | nullrequiredThe flagger's note. Null when none
audioobjectrequiredThe line's place in the call recording. Transcribers hear it with 0.75 s more on each side, so play `startSeconds - 0.75` to `endSeconds + 0.75`, clamped to the recording, to hear what they heard
Show 2 child properties
startSecondsnumberrequiredWhere the line starts, in seconds from the recording start
endSecondsnumberrequiredWhere the line ends, in seconds from the recording start
evaluationenumrequiredpending: candidate attempts are queued and not all reported, or the turn was created in no bank. scored: every transcriber its job asked for returned an attempt (a failed attempt counts), or it joined a bank that owed it no attempt. failed: its job ended without an attempt from every transcriber it asked for. A turn taken out of a bank keeps its evaluation, and a backfill does not move a scored turn back to pending: watch `evaluationProgress` for that work
"pending""scored""failed"controlForTurnIdstring | nullrequiredOn a control, the flag it was sampled for. Deleting or moving that flag deletes or moves the control. Null otherwise
fullCallRunIdstring | nullrequiredOn a full-call turn, its run. Null otherwise
tieBreakobject | nullrequiredWhen two or more candidates match the expected text with different raw text (casing, punctuation), the one an LLM judge found fits the venue best. Null otherwise
Show 2 child properties
transcriberIdstringrequiredThe candidate whose text the judge picked
reasonstringrequiredThe judge's one-sentence reason
labelenum | nullrequiredOn a full-call turn where the candidate differs from production, its label: decided by the expected-text matches where the line has expected text, else by the LLM judge. candidate_right: the candidate has what the caller said and production does not. production_right: production has it and the candidate does not. same_meaning: they differ in a way that does not matter. both_wrong: neither has it. Null otherwise or until the analysis labels it
"candidate_right""production_right""same_meaning""both_wrong"reviewenum | nullrequiredOn a full-call turn, a person's verdict from POST /full-call-runs/:id/reviews, in the same values as `label`. It counts only on differing turns, and outranks the label in `fixed` and `introduced` only where the line has no expected text. Null until reviewed
"candidate_right""production_right""same_meaning""both_wrong"calibrationbooleanrequiredOn a full-call turn, whether production is also replayed on it to measure replay against the live text. False otherwise
createdBystring | nullrequiredWho made it, as the creating request named them in an `X-On-Behalf-Of` header. Null when it sent none
createdAtstring (date-time) | string (date) | number | nullrequiredWhen it was created, UTC
updatedAtstring (date-time) | string (date) | number | nullrequiredWhen it last changed, UTC
liveTranscriberobject | nullrequiredThe transcriber the turn's call ran on, recorded when the turn was created. Its stored text is the turn's production attempt. Null when unknown
Show 2 child properties
idstringrequiredTranscriber id, as GET /transcribers lists it
namestringrequiredThe transcriber's display name
evaluationProgressobject | nullrequiredThe newest open transcribe job holding the turn, for a progress bar. Null when no open job holds it (scored, failed or never queued), and on the POST /turn-flags response, which is written before its job is queued: read the turn again to see it
Show 3 child properties
stageenumrequiredwaiting: the job is queued or dispatched and no worker has started it. scoring: a worker is running it
"waiting""scoring"donestring (integer) | integerrequiredTranscriptions the job has finished, over every clip and transcriber it holds, not this turn's alone. 0 until the worker first reports
totalstring (integer) | integerrequiredTranscriptions the job holds in all
metadataobjectoptionalRequest metadata
Show 3 child properties
timestampstringrequiredWhen the server answered, ISO 8601 UTC
versionstringrequiredAPI version
pathstringoptionalThe URL that was requested
400VALIDATION_ERROR: a path id, query parameter or body field broke a rule; `details` lists each `field` and its `message`. A body with a missing field or a JSON type the schema does not allow answers 400 with code INTERNAL_SERVER_ERROR and no `details` instead, so branch on the status
successbooleanrequiredfalse on every error
errorobjectoptionalWhat went wrong: a stable `code` to branch on and a human-readable `message`
Show 3 child properties
codeenumrequiredA parameter or body field broke a rule
"VALIDATION_ERROR"messagestringrequiredHuman-readable explanation. Do not parse it; branch on `code`
detailsobject[]optionalOne entry per field that failed
Show 2 item properties
fieldstringrequiredThe offending field
messagestringrequiredThe rule it broke
metadataobjectoptionalRequest metadata
Show 3 child properties
timestampstringrequiredWhen the server answered, ISO 8601 UTC
versionstringrequiredAPI version
pathstringoptionalThe URL that was requested
401UNAUTHORIZED: no valid credentials, or a dashboard token that names no organization. Send `Authorization: ApiKey <organization API key>`
successbooleanrequiredfalse on every error
errorobjectoptionalWhat went wrong: a stable `code` to branch on and a human-readable `message`
Show 2 child properties
codeenumrequiredNo valid credentials
"UNAUTHORIZED"messagestringrequiredHuman-readable explanation. Do not parse it; branch on `code`
metadataobjectoptionalRequest metadata
Show 3 child properties
timestampstringrequiredWhen the server answered, ISO 8601 UTC
versionstringrequiredAPI version
pathstringoptionalThe URL that was requested
403ORG_SCOPE_REQUIRED: transcriber testing is an organization feature. Use an organization API key without `X-Tenant-Id`, or a dashboard token scoped to the organization. A tenant API key, an organization key sent with `X-Tenant-Id` for one of its tenants, or a token carrying a tenant gets this 403. FORBIDDEN: an organization key sent `X-Tenant-Id` for a tenant outside its organization
successbooleanrequiredfalse on every error
errorobjectoptionalWhat went wrong: a stable `code` to branch on and a human-readable `message`
Show 2 child properties
codeenumrequiredORG_SCOPE_REQUIRED: the credentials carry a tenant scope. FORBIDDEN: an organization key sent `X-Tenant-Id` for a tenant outside its organization
"ORG_SCOPE_REQUIRED""FORBIDDEN"messagestringrequiredHuman-readable explanation. Do not parse it; branch on `code`
metadataobjectoptionalRequest metadata
Show 3 child properties
timestampstringrequiredWhen the server answered, ISO 8601 UTC
versionstringrequiredAPI version
pathstringoptionalThe URL that was requested
404RESOURCE_NOT_FOUND: an id in the path, query or body names nothing in the caller's organization. `message` names what, e.g. "Transcriber bank not found"
successbooleanrequiredfalse on every error
errorobjectoptionalWhat went wrong: a stable `code` to branch on and a human-readable `message`
Show 2 child properties
codeenumrequiredAn id names nothing the caller can see
"RESOURCE_NOT_FOUND"messagestringrequiredHuman-readable explanation. Do not parse it; branch on `code`
metadataobjectoptionalRequest metadata
Show 3 child properties
timestampstringrequiredWhen the server answered, ISO 8601 UTC
versionstringrequiredAPI version
pathstringoptionalThe URL that was requested
409TURN_FLAG_EXISTS: the line already has a live flag; edit it with PATCH /turn-flags/:id using `details.existingFlagId`
successbooleanrequiredfalse on every error
errorobjectoptionalWhat went wrong: a stable `code` to branch on and a human-readable `message`
Show 3 child properties
codeenumrequiredThe line already has a flag
"TURN_FLAG_EXISTS"messagestringrequiredHuman-readable explanation. Do not parse it; branch on `code`
detailsobjectrequiredThe flag in the way
Show 1 child properties
existingFlagIdstring | nullrequiredThe line's live flag, to PATCH instead. Null if it was deleted meanwhile
metadataobjectoptionalRequest metadata
Show 3 child properties
timestampstringrequiredWhen the server answered, ISO 8601 UTC
versionstringrequiredAPI version
pathstringoptionalThe URL that was requested
422LINE_NOT_FLAGGABLE: the line cannot be flagged; `details.reason` says why, as `notFlaggableReason` does on GET /turn-flags/calls/:callId/lines
successbooleanrequiredfalse on every error
errorobjectoptionalWhat went wrong: a stable `code` to branch on and a human-readable `message`
Show 3 child properties
codeenumrequiredThe line cannot be flagged
"LINE_NOT_FLAGGABLE"messagestringrequiredHuman-readable explanation. Do not parse it; branch on `code`
detailsobjectrequiredWhy
Show 1 child properties
reasonenumrequiredWhy a line cannot be flagged, the first that holds of: not_caller_line (an assistant line), no_recording (the call has no recording), transcriber_unknown (the call records no transcriber and its cost breakdown names none uniquely, so there is no live transcriber to score against), no_audio_bounds (the line has no start and end in the recording)
"not_caller_line""no_audio_bounds""no_recording""transcriber_unknown"metadataobjectoptionalRequest metadata
Show 3 child properties
timestampstringrequiredWhen the server answered, ISO 8601 UTC
versionstringrequiredAPI version
pathstringoptionalThe URL that was requested
429BACKLOG_FULL: the organization has more than `details.limit` (50) transcriber jobs waiting. Nothing was written; retry once some finish
successbooleanrequiredfalse on every error
errorobjectoptionalWhat went wrong: a stable `code` to branch on and a human-readable `message`
Show 3 child properties
codeenumrequiredToo many jobs waiting
"BACKLOG_FULL"messagestringrequiredHuman-readable explanation. Do not parse it; branch on `code`
detailsobjectrequiredThe limit hit
Show 1 child properties
limitstring (integer) | integerrequiredPending jobs the organization may hold
metadataobjectoptionalRequest metadata
Show 3 child properties
timestampstringrequiredWhen the server answered, ISO 8601 UTC
versionstringrequiredAPI version
pathstringoptionalThe URL that was requested
500An unexpected server error
successbooleanrequiredfalse on every error
errorobjectoptionalWhat went wrong: a stable `code` to branch on and a human-readable `message`
Show 2 child properties
codeenumrequiredAn unexpected server failure
"INTERNAL_SERVER_ERROR"messagestringrequiredHuman-readable explanation. Do not parse it; branch on `code`
metadataobjectoptionalRequest metadata
Show 3 child properties
timestampstringrequiredWhen the server answered, ISO 8601 UTC
versionstringrequiredAPI version
pathstringoptionalThe URL that was requested