The Speak AI API exposes 10 shared links endpoints under the base URL https://api.speakai.co/v1. These requests take no API key or access token: the share link token in ?token= authorizes them.
The routes a dashboard’s share link uses to read and write labels and comments on its media, and that a media or folder embed uses to read them. There is no signed-in user: the link token in ?token= is the only credential, so these routes take no API key or access token.
Embeds
Media and folder embeds have meta.isLabels and meta.isComments, both off by default and set with PUT /embed/{embedId}. Embeds are read-only: the two GET routes answer an embed token only while its flag is on, for the embed’s own media or media in its folders, and every write route refuses an embed token.
Refusals
Every refusal (unknown or revoked token, expired subscription, the setting off, view mode on a write, media outside the dashboard’s folders, a name that is not allowed, someone else’s entry) returns the same 404 {"message": "Not found."}, so a link holder cannot tell which check failed. Validation errors still return 400 and a stale transcript revision 409. Reads allow 120 requests a minute per IP address and writes 30; past that the routes return 429.
Writing as a reviewer
Writes need labels.mode set to apply or comments.mode set to reply on the dashboard, and send a reviewerName from annotationSettings.reviewers, the dashboard’s feedback submitter names. When allowOtherReviewer is true (the dashboard’s feedback.allowOtherSubmitter is on), a name that is not listed is also accepted. With no names listed and other names off, nobody can write. Link writes are not authenticated: the link holder chooses the name, and Speak AI records the entry under it without checking who sent it, the same as feedback. Only the dashboard’s feedback names are accepted, plus any other name when other names are allowed. Speak AI accepts this by design. Every entry is shown as made via the dashboard under the chosen name, a name can change or delete only the entries written under it from that dashboard, and revoking the link stops everything at once. Link responses carry the author’s display name only. Linking a comment to a passage, moving passages and resolving threads stay in the app.
What can you do with the shared links endpoints?
Speak AI groups these 10 endpoints under the shared links resource. Each entry below links to the full reference for that endpoint further down this page.
Labels on a transcript
Read a shared media page
/embed/insightRead the media page a share link opens: the media, its transcript and insights. For a dashboard link it also carries annotationSettings, and the media must be in one of the dashboard’s folders. mediaData.transcriptRevision is read with the transcript: place labels and comments only while it matches the transcriptRevision from the labels and comments routes, and read again otherwise. This response is never cached. No API key or access token: the token is the only credential.
Parameters
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
token |
query | string | Yes | The share link token of a dashboard, or of a media or folder embed on the read routes. Up to 200 characters. |
mediaId |
query | string | No | The media to open. Required for a dashboard link. |
curl -X GET 'https://api.speakai.co/v1/embed/insight?token=dsh_3kQm9XpL2vRt&mediaId=b4994aa1267c'200 Success
Response body, application/json.
| Field | Type | Description |
|---|---|---|
status |
string | success on every 2xx response. One of: success . |
data |
object | The media page. Only the keys relevant to labels, comments and the thumbnail are listed here. |
data.mediaData |
object | |
data.annotationSettings |
object | What the link allows for labels and comments, resolved from the dashboard’s settings. Present for dashboard links only. |
data.mediaData – 2 more fields
| Field | Type | Description |
|---|---|---|
transcriptRevision |
integer | Revision of the transcript in this response. 0 for media saved before revisions existed. |
thumbnailUrl |
string | A signed link to the video’s thumbnail, custom or automatic. Present only when the file has one. It expires, so do not store it. |
data.annotationSettings – 4 more fields
| Field | Type | Description |
|---|---|---|
labels |
object | |
labels.isEnabled |
boolean | Whether labels are on. |
labels.mode |
string | view or apply. One of: view, apply . |
labels.groups |
object[] | The allowed label groups. Empty while labels are off. |
labels.labels |
object[] | Active labels of the allowed groups. Empty while labels are off. |
comments |
object | |
comments.isEnabled |
boolean | Whether comments are on. |
comments.mode |
string | view or reply. One of: view, reply . |
reviewers |
string[] | Names a viewer may write as, sent as reviewerName: the dashboard’s feedback submitter names. Empty unless apply or reply mode is on. |
allowOtherReviewer |
boolean | A viewer may also type a name that is not listed. True when the dashboard’s feedback.allowOtherSubmitter is on and apply or reply mode is on. |
labels.groups – 2 more fields
| Field | Type |
|---|---|
labelId |
string |
name |
string |
labels.labels – 5 more fields
| Field | Type | Description |
|---|---|---|
labelId |
string | Id of the label. |
name |
string | Name of the label. |
color |
string | Hex colour, such as #6366f1. |
description |
string | Description of the label. |
parentId |
string,null | Id of the label’s group, or null. |
403 The media is not in the dashboard’s folders
Response body, application/json.
| Field | Type | Description |
|---|---|---|
status |
string | failed. One of: failed . |
requestId |
string | Id of this request, for support. |
code |
integer | The HTTP status code again. |
message |
string | What went wrong, in words you can show to a person. |
hints |
string[] | Generic hints for this kind of error. |
404 Unknown token or no active subscription
Response body, application/json.
| Field | Type | Description |
|---|---|---|
status |
string | failed. One of: failed . |
requestId |
string | Id of this request, for support. |
code |
integer | The HTTP status code again. |
message |
string | What went wrong, in words you can show to a person. |
hints |
string[] | Generic hints for this kind of error. |
Delete a comment through a share link
/embed/media/{mediaId}/comments/{commentId}Delete your own comment through a dashboard share link. Allowed while comments are on with comments.mode set to reply. No API key or access token: the share link token is the only credential. The media must be in one of the dashboard’s folders. Every refusal returns the same 404 {"message": "Not found."}, so a link holder cannot tell which check failed. The entry is recorded under reviewerName via this dashboard. The name is chosen by the link holder and not authenticated, the same as feedback; only a name the dashboard allows is accepted. A name can change or delete only the entries written under it from the same dashboard. Writes are limited to 30 requests a minute per IP address.
Parameters
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
mediaId |
path | string | Yes | Id of the media file. Trimmed, then 1 to 64 characters from A-Z, a-z, 0-9, _ and - (pattern ^[A-Za-z0-9_-]{1,64}$). Anything else returns 400. |
commentId |
path | string | Yes | Id of the comment. Trimmed, then 1 to 64 characters from A-Z, a-z, 0-9, _ and - (pattern ^[A-Za-z0-9_-]{1,64}$). Anything else returns 400. |
token |
query | string | Yes | The share link token of a dashboard, or of a media or folder embed on the read routes. Up to 200 characters. |
Request body
Fields marked required are the ones the server rejects the request without. Anything conditional, where a field becomes required only alongside another, is described under Request rules above rather than marked here.
| Field | Type | Description |
|---|---|---|
reviewerName |
string, required | The name you write as: one of annotationSettings.reviewers, or any name when allowOtherReviewer is true. Chosen by the link holder and not authenticated. Trimmed; a listed name in another case is stored with the listed spelling. Required. |
curl -X DELETE 'https://api.speakai.co/v1/embed/media/b4994aa1267c/comments/Cm8WpE3kLv6NdHa?token=dsh_3kQm9XpL2vRt' \
-H 'Content-Type: application/json' \
-d '{
"reviewerName": "Reviewer A"
}'200 Success
Response body, application/json.
| Field | Type | Description |
|---|---|---|
status |
string | success on every 2xx response. One of: success . |
data |
object | The result. |
data.commentId |
string | Id of the comment you deleted. |
Example response (Success), application/json.
{
"status": "success",
"data": {
"commentId": "Cm8WpE3kLv6NdHa"
}
}404 Any refusal: unknown or revoked token, expired subscription, the setting is off, view mode on a write, media outside the link, a name that is not allowed, or someone else’s entry
Response body, application/json.
| Field | Type | Description |
|---|---|---|
status |
string | failed. One of: failed . |
requestId |
string | Id of this request, for support. |
code |
integer | The HTTP status code again. |
message |
string | What went wrong, in words you can show to a person. |
hints |
string[] | Generic hints for this kind of error. |
429 Rate limit reached
The spec records this status code with no example body.
Read a shared dashboard’s layout and annotation settings
/embed/dashboards/configRead what a dashboard share link renders before any media is opened: its title, sections, widgets and annotationSettings, which lists the labels the “has label” filter may offer. No API key or access token: the token is the only credential. Changes to the dashboard’s settings reach the link within about 15 seconds.
Parameters
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
token |
query | string | Yes | The share link token of a dashboard, or of a media or folder embed on the read routes. Up to 200 characters. |
curl -X GET 'https://api.speakai.co/v1/embed/dashboards/config?token=dsh_3kQm9XpL2vRt'200 Success
Response body, application/json.
| Field | Type | Description |
|---|---|---|
status |
string | success on every 2xx response. One of: success . |
data |
object | The dashboard’s public layout. Only the keys relevant to labels and comments are listed here. |
data.title |
string | Dashboard title. |
data.annotationSettings |
object | What the link allows for labels and comments, resolved from the dashboard’s settings. |
data.version |
string | Changes whenever the dashboard changes. |
data.annotationSettings – 4 more fields
| Field | Type | Description |
|---|---|---|
labels |
object | |
labels.isEnabled |
boolean | Whether labels are on. |
labels.mode |
string | view or apply. One of: view, apply . |
labels.groups |
object[] | The allowed label groups. Empty while labels are off. |
labels.labels |
object[] | Active labels of the allowed groups. Empty while labels are off. |
comments |
object | |
comments.isEnabled |
boolean | Whether comments are on. |
comments.mode |
string | view or reply. One of: view, reply . |
reviewers |
string[] | Names a viewer may write as, sent as reviewerName: the dashboard’s feedback submitter names. Empty unless apply or reply mode is on. |
allowOtherReviewer |
boolean | A viewer may also type a name that is not listed. True when the dashboard’s feedback.allowOtherSubmitter is on and apply or reply mode is on. |
labels.groups – 2 more fields
| Field | Type |
|---|---|
labelId |
string |
name |
string |
labels.labels – 5 more fields
| Field | Type | Description |
|---|---|---|
labelId |
string | Id of the label. |
name |
string | Name of the label. |
color |
string | Hex colour, such as #6366f1. |
description |
string | Description of the label. |
parentId |
string,null | Id of the label’s group, or null. |
404 Unknown or revoked token
Response body, application/json.
| Field | Type | Description |
|---|---|---|
status |
string | failed. One of: failed . |
requestId |
string | Id of this request, for support. |
code |
integer | The HTTP status code again. |
message |
string | What went wrong, in words you can show to a person. |
hints |
string[] | Generic hints for this kind of error. |
List comments through a share link
/embed/media/{mediaId}/commentsRead every comment thread on a media file through a dashboard share link. Allowed while the dashboard’s comments.isEnabled is true. resolvedBy and mediaLabelId are left out. No API key or access token: the share link token is the only credential. The media must be in one of the dashboard’s folders. Every refusal returns the same 404 {"message": "Not found."}, so a link holder cannot tell which check failed. A media or folder embed token also works here while the embed’s meta.isComments is true, for the embed’s own media or media in its folders; an embed shows every active label and can never write. Reads are limited to 120 requests a minute per IP address.
Parameters
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
mediaId |
path | string | Yes | Id of the media file. Trimmed, then 1 to 64 characters from A-Z, a-z, 0-9, _ and - (pattern ^[A-Za-z0-9_-]{1,64}$). Anything else returns 400. |
token |
query | string | Yes | The share link token of a dashboard, or of a media or folder embed on the read routes. Up to 200 characters. |
curl -X GET 'https://api.speakai.co/v1/embed/media/b4994aa1267c/comments?token=dsh_3kQm9XpL2vRt'200 Success
Response body, application/json.
| Field | Type | Description |
|---|---|---|
status |
string | success on every 2xx response. One of: success . |
data |
object | The result. |
data.transcriptRevision |
integer | The media’s current transcript revision. |
data.threads |
object[] | Threads, oldest first. |
data.threads – 13 more fields
| Field | Type | Description |
|---|---|---|
commentId |
string | Id of the comment. |
mediaId |
string | Id of the media. |
anchor |
object | The passage the comment is on, or null for a comment on the whole file. Replies carry null. |
parentId |
string | commentId of the thread’s first comment for a reply, null for the first comment. |
body |
string | Comment text. Empty on a deleted first comment that still has replies. |
status |
string | Present only on comments with an anchor. active: the same words, possibly at new positions after an edit. shifted: most of the words survived an edit near the same time, but the text changed. needs_review: the words changed too much to follow; anchor then holds a suggested passage on the new transcript and lastResolved the last position a person confirmed. One of: active, shifted, needs_review . |
lastResolved |
object | Present only while status is needs_review: the last position a person confirmed. |
isResolved |
boolean | true once the thread is resolved. Only set on the first comment of a thread. |
isDeleted |
boolean | true on a deleted first comment kept because it has replies. |
createdAt |
string (date-time) | When it was posted. |
updatedAt |
string (date-time) | When it last changed. |
replies |
object[] | Replies, oldest first, with the same fields. Deleted replies are left out. |
authorName |
string | The author’s display name: the name a share-link viewer wrote as, or the team member’s name, or “Team member” when a member has none or has left the company. |
anchor – 9 more fields
| Field | Type | Description |
|---|---|---|
startWord |
integer | Position of the first word. |
endWord |
integer | Position of the last word, inclusive. |
exact |
string | The quoted words, joined by single spaces. |
prefix |
string | Up to 32 characters of transcript text before the quote. |
suffix |
string | Up to 32 characters of transcript text after the quote. |
startInSec |
number | Start time of the first word, in seconds. |
endInSec |
number | End time of the last word, in seconds. |
speakerIds |
string[] | Speakers of the quoted words. |
transcriptRevision |
integer | The transcript revision these positions refer to. |
lastResolved – 9 more fields
| Field | Type | Description |
|---|---|---|
startWord |
integer | Position of the first word. |
endWord |
integer | Position of the last word, inclusive. |
exact |
string | The quoted words, joined by single spaces. |
prefix |
string | Up to 32 characters of transcript text before the quote. |
suffix |
string | Up to 32 characters of transcript text after the quote. |
startInSec |
number | Start time of the first word, in seconds. |
endInSec |
number | End time of the last word, in seconds. |
speakerIds |
string[] | Speakers of the quoted words. |
transcriptRevision |
integer | The transcript revision these positions refer to. |
replies – 12 more fields
| Field | Type | Description |
|---|---|---|
commentId |
string | Id of the comment. |
mediaId |
string | Id of the media. |
anchor |
object | The passage the comment is on, or null for a comment on the whole file. Replies carry null. |
parentId |
string | commentId of the thread’s first comment for a reply, null for the first comment. |
body |
string | Comment text. Empty on a deleted first comment that still has replies. |
status |
string | Present only on comments with an anchor. active: the same words, possibly at new positions after an edit. shifted: most of the words survived an edit near the same time, but the text changed. needs_review: the words changed too much to follow; anchor then holds a suggested passage on the new transcript and lastResolved the last position a person confirmed. One of: active, shifted, needs_review . |
lastResolved |
object | Present only while status is needs_review: the last position a person confirmed. |
isResolved |
boolean | true once the thread is resolved. Only set on the first comment of a thread. |
isDeleted |
boolean | true on a deleted first comment kept because it has replies. |
createdAt |
string (date-time) | When it was posted. |
updatedAt |
string (date-time) | When it last changed. |
authorName |
string | The author’s display name: the name a share-link viewer wrote as, or the team member’s name, or “Team member” when a member has none or has left the company. |
anchor – 9 more fields
| Field | Type | Description |
|---|---|---|
startWord |
integer | Position of the first word. |
endWord |
integer | Position of the last word, inclusive. |
exact |
string | The quoted words, joined by single spaces. |
prefix |
string | Up to 32 characters of transcript text before the quote. |
suffix |
string | Up to 32 characters of transcript text after the quote. |
startInSec |
number | Start time of the first word, in seconds. |
endInSec |
number | End time of the last word, in seconds. |
speakerIds |
string[] | Speakers of the quoted words. |
transcriptRevision |
integer | The transcript revision these positions refer to. |
lastResolved – 9 more fields
| Field | Type | Description |
|---|---|---|
startWord |
integer | Position of the first word. |
endWord |
integer | Position of the last word, inclusive. |
exact |
string | The quoted words, joined by single spaces. |
prefix |
string | Up to 32 characters of transcript text before the quote. |
suffix |
string | Up to 32 characters of transcript text after the quote. |
startInSec |
number | Start time of the first word, in seconds. |
endInSec |
number | End time of the last word, in seconds. |
speakerIds |
string[] | Speakers of the quoted words. |
transcriptRevision |
integer | The transcript revision these positions refer to. |
Example response (Success), application/json.
{
"status": "success",
"data": {
"transcriptRevision": 3,
"threads": [
{
"commentId": "Cm8WpE3kLv6NdHa",
"mediaId": "b4994aa1267c",
"anchor": {
"startWord": 42,
"endWord": 49,
"exact": "we need the pricing sheet by Friday",
"prefix": "Before we close the quarter, ",
"suffix": ". Can you send it over?",
"startInSec": 61.4,
"endInSec": 64.2,
"speakerIds": [
"1"
],
"transcriptRevision": 3
},
"parentId": null,
"body": "Can we confirm this date with finance?",
"status": "active",
"isResolved": false,
"isDeleted": false,
"createdAt": "2026-10-06T14:05:12.000Z",
"updatedAt": "2026-10-06T14:05:12.000Z",
"replies": [
{
"commentId": "Cm2YsF9qJt4BxMr",
"mediaId": "b4994aa1267c",
"anchor": null,
"parentId": "Cm8WpE3kLv6NdHa",
"body": "Confirmed, Friday works.",
"isResolved": false,
"isDeleted": false,
"createdAt": "2026-10-06T15:20:40.000Z",
"updatedAt": "2026-10-06T15:20:40.000Z",
"authorName": "Reviewer A"
}
],
"authorName": "Reviewer A"
}
]
}
}400 A path or query value failed validation
Response body, application/json.
| Field | Type | Description |
|---|---|---|
status |
string | failed. One of: failed . |
requestId |
string | Id of this request, for support. |
code |
integer | The HTTP status code again. |
message |
string | What went wrong, in words you can show to a person. |
hints |
string[] | Generic hints for this kind of error. |
Example response (A path or query value failed validation), application/json.
{
"status": "failed",
"requestId": "req_0000000000000000",
"code": 400,
"message": "\"filter\" must be one of [all, open, resolved, file]",
"hints": [
"The API request is invalid or improperly formed."
]
}404 Any refusal: unknown or revoked token, expired subscription, the setting is off, view mode on a write, media outside the link, a name that is not allowed, or someone else’s entry
Response body, application/json.
| Field | Type | Description |
|---|---|---|
status |
string | failed. One of: failed . |
requestId |
string | Id of this request, for support. |
code |
integer | The HTTP status code again. |
message |
string | What went wrong, in words you can show to a person. |
hints |
string[] | Generic hints for this kind of error. |
429 Rate limit reached
The spec records this status code with no example body.
Comment through a share link
/embed/media/{mediaId}/commentsStart a thread or reply through a dashboard share link. Allowed while comments are on with comments.mode set to reply. Send range with expectedTranscriptRevision to comment on a passage, parentId to reply, or neither to comment on the whole file. mediaLabelId is not accepted here. No API key or access token: the share link token is the only credential. The media must be in one of the dashboard’s folders. Every refusal returns the same 404 {"message": "Not found."}, so a link holder cannot tell which check failed. The entry is recorded under reviewerName via this dashboard. The name is chosen by the link holder and not authenticated, the same as feedback; only a name the dashboard allows is accepted. A name can change or delete only the entries written under it from the same dashboard. Writes are limited to 30 requests a minute per IP address.
Parameters
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
mediaId |
path | string | Yes | Id of the media file. Trimmed, then 1 to 64 characters from A-Z, a-z, 0-9, _ and - (pattern ^[A-Za-z0-9_-]{1,64}$). Anything else returns 400. |
token |
query | string | Yes | The share link token of a dashboard, or of a media or folder embed on the read routes. Up to 200 characters. |
Request body
Fields marked required are the ones the server rejects the request without. Anything conditional, where a field becomes required only alongside another, is described under Request rules above rather than marked here.
| Field | Type | Description |
|---|---|---|
reviewerName |
string, required | The name you write as: one of annotationSettings.reviewers, or any name when allowOtherReviewer is true. Chosen by the link holder and not authenticated. Trimmed; a listed name in another case is stored with the listed spelling. Required. |
body |
string, required | Comment text. Trimmed, then 1 to 5,000 characters. |
range |
object | The passage to comment on. Send it with expectedTranscriptRevision; leave both out for a whole-file comment. Not allowed on a reply. Inclusive range of word positions. Word positions count every word of the transcript in order, starting at 0. Within each sentence, words come from entities in order (an entity that holds several words, such as “New York”, counts as several), or from the sentence text split on whitespace when the sentence has no entities. A token that is only punctuation is not counted. |
range.start |
integer, required | Position of the first word. Integer, 0 or more. |
range.end |
integer, required | Position of the last word, inclusive. Integer, at least start. A range that runs past the last word returns 400 “The selected words are outside this transcript.” |
expectedTranscriptRevision |
integer | Required when you send range, not allowed without it. The transcriptRevision you read the transcript at. Integer, 0 or more. A stale value returns 409. |
parentId |
string | commentId of the thread’s first comment, to post a reply. Trimmed, then 1 to 64 characters from A-Z, a-z, 0-9, _ and - (pattern ^[A-Za-z0-9_-]{1,64}$). Anything else returns 400. |
curl -X POST 'https://api.speakai.co/v1/embed/media/b4994aa1267c/comments?token=dsh_3kQm9XpL2vRt' \
-H 'Content-Type: application/json' \
-d '{
"reviewerName": "Reviewer A",
"body": "Can we confirm this date with finance?",
"range": {
"start": 42,
"end": 49
},
"expectedTranscriptRevision": 3
}'200 Success
Response body, application/json.
| Field | Type | Description |
|---|---|---|
status |
string | success on every 2xx response. One of: success . |
data |
object | A comment. |
data – 12 more fields
| Field | Type | Description |
|---|---|---|
commentId |
string | Id of the comment. |
mediaId |
string | Id of the media. |
anchor |
object | The passage the comment is on, or null for a comment on the whole file. Replies carry null. |
parentId |
string | commentId of the thread’s first comment for a reply, null for the first comment. |
body |
string | Comment text. Empty on a deleted first comment that still has replies. |
status |
string | Present only on comments with an anchor. active: the same words, possibly at new positions after an edit. shifted: most of the words survived an edit near the same time, but the text changed. needs_review: the words changed too much to follow; anchor then holds a suggested passage on the new transcript and lastResolved the last position a person confirmed. One of: active, shifted, needs_review . |
lastResolved |
object | Present only while status is needs_review: the last position a person confirmed. |
isResolved |
boolean | true once the thread is resolved. Only set on the first comment of a thread. |
isDeleted |
boolean | true on a deleted first comment kept because it has replies. |
createdAt |
string (date-time) | When it was posted. |
updatedAt |
string (date-time) | When it last changed. |
authorName |
string | The author’s display name: the name a share-link viewer wrote as, or the team member’s name, or “Team member” when a member has none or has left the company. |
anchor – 9 more fields
| Field | Type | Description |
|---|---|---|
startWord |
integer | Position of the first word. |
endWord |
integer | Position of the last word, inclusive. |
exact |
string | The quoted words, joined by single spaces. |
prefix |
string | Up to 32 characters of transcript text before the quote. |
suffix |
string | Up to 32 characters of transcript text after the quote. |
startInSec |
number | Start time of the first word, in seconds. |
endInSec |
number | End time of the last word, in seconds. |
speakerIds |
string[] | Speakers of the quoted words. |
transcriptRevision |
integer | The transcript revision these positions refer to. |
lastResolved – 9 more fields
| Field | Type | Description |
|---|---|---|
startWord |
integer | Position of the first word. |
endWord |
integer | Position of the last word, inclusive. |
exact |
string | The quoted words, joined by single spaces. |
prefix |
string | Up to 32 characters of transcript text before the quote. |
suffix |
string | Up to 32 characters of transcript text after the quote. |
startInSec |
number | Start time of the first word, in seconds. |
endInSec |
number | End time of the last word, in seconds. |
speakerIds |
string[] | Speakers of the quoted words. |
transcriptRevision |
integer | The transcript revision these positions refer to. |
Example response (Success), application/json.
{
"status": "success",
"data": {
"commentId": "Cm8WpE3kLv6NdHa",
"mediaId": "b4994aa1267c",
"anchor": {
"startWord": 42,
"endWord": 49,
"exact": "we need the pricing sheet by Friday",
"prefix": "Before we close the quarter, ",
"suffix": ". Can you send it over?",
"startInSec": 61.4,
"endInSec": 64.2,
"speakerIds": [
"1"
],
"transcriptRevision": 3
},
"parentId": null,
"body": "Can we confirm this date with finance?",
"status": "active",
"isResolved": false,
"isDeleted": false,
"createdAt": "2026-10-06T14:05:12.000Z",
"updatedAt": "2026-10-06T14:05:12.000Z",
"authorName": "Reviewer A"
}
}400 Validation, reply depth, range or limit problem
Response body, application/json.
| Field | Type | Description |
|---|---|---|
status |
string | failed. One of: failed . |
requestId |
string | Id of this request, for support. |
code |
integer | The HTTP status code again. |
message |
string | What went wrong, in words you can show to a person. |
hints |
string[] | Generic hints for this kind of error. |
Example response (Validation, reply depth, range or limit problem), application/json.
{
"status": "failed",
"requestId": "req_0000000000000000",
"code": 400,
"message": "Replies can only be added to the first comment in a thread.",
"hints": [
"The API request is invalid or improperly formed."
]
}404 Any refusal: unknown or revoked token, expired subscription, the setting is off, view mode on a write, media outside the link, a name that is not allowed, or someone else’s entry
Response body, application/json.
| Field | Type | Description |
|---|---|---|
status |
string | failed. One of: failed . |
requestId |
string | Id of this request, for support. |
code |
integer | The HTTP status code again. |
message |
string | What went wrong, in words you can show to a person. |
hints |
string[] | Generic hints for this kind of error. |
409 The transcript changed since you read it
Response body, application/json.
| Field | Type | Description |
|---|---|---|
status |
string | failed. One of: failed . |
requestId |
string | Id of this request, for support. |
code |
integer | The HTTP status code again. |
message |
string | What went wrong, in words you can show to a person. |
hints |
string[] | Generic hints for this kind of error. |
Example response (The transcript changed since you read it), application/json.
{
"status": "failed",
"requestId": "req_0000000000000000",
"code": 409,
"message": "This transcript changed since you opened it. Reload to keep editing.",
"hints": [
"Please use PUT for update instead of POST",
"The requested operation failed because it tried to create a resource that already exists."
]
}429 Rate limit reached
The spec records this status code with no example body.
Edit a comment through a share link
/embed/media/{mediaId}/comments/{commentId}Edit the text of your own comment through a dashboard share link. Allowed while comments are on with comments.mode set to reply. Resolving threads stays in the app. No API key or access token: the share link token is the only credential. The media must be in one of the dashboard’s folders. Every refusal returns the same 404 {"message": "Not found."}, so a link holder cannot tell which check failed. The entry is recorded under reviewerName via this dashboard. The name is chosen by the link holder and not authenticated, the same as feedback; only a name the dashboard allows is accepted. A name can change or delete only the entries written under it from the same dashboard. Writes are limited to 30 requests a minute per IP address.
Parameters
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
mediaId |
path | string | Yes | Id of the media file. Trimmed, then 1 to 64 characters from A-Z, a-z, 0-9, _ and - (pattern ^[A-Za-z0-9_-]{1,64}$). Anything else returns 400. |
commentId |
path | string | Yes | Id of the comment. Trimmed, then 1 to 64 characters from A-Z, a-z, 0-9, _ and - (pattern ^[A-Za-z0-9_-]{1,64}$). Anything else returns 400. |
token |
query | string | Yes | The share link token of a dashboard, or of a media or folder embed on the read routes. Up to 200 characters. |
Request body
Fields marked required are the ones the server rejects the request without. Anything conditional, where a field becomes required only alongside another, is described under Request rules above rather than marked here.
| Field | Type | Description |
|---|---|---|
reviewerName |
string, required | The name you write as: one of annotationSettings.reviewers, or any name when allowOtherReviewer is true. Chosen by the link holder and not authenticated. Trimmed; a listed name in another case is stored with the listed spelling. Required. |
body |
string, required | New text. Author only. Comment text. Trimmed, then 1 to 5,000 characters. |
curl -X PATCH 'https://api.speakai.co/v1/embed/media/b4994aa1267c/comments/Cm8WpE3kLv6NdHa?token=dsh_3kQm9XpL2vRt' \
-H 'Content-Type: application/json' \
-d '{
"reviewerName": "Reviewer A",
"body": "Agreed, follow up on pricing."
}'200 Success
Response body, application/json.
| Field | Type | Description |
|---|---|---|
status |
string | success on every 2xx response. One of: success . |
data |
object | A comment. |
data – 12 more fields
| Field | Type | Description |
|---|---|---|
commentId |
string | Id of the comment. |
mediaId |
string | Id of the media. |
anchor |
object | The passage the comment is on, or null for a comment on the whole file. Replies carry null. |
parentId |
string | commentId of the thread’s first comment for a reply, null for the first comment. |
body |
string | Comment text. Empty on a deleted first comment that still has replies. |
status |
string | Present only on comments with an anchor. active: the same words, possibly at new positions after an edit. shifted: most of the words survived an edit near the same time, but the text changed. needs_review: the words changed too much to follow; anchor then holds a suggested passage on the new transcript and lastResolved the last position a person confirmed. One of: active, shifted, needs_review . |
lastResolved |
object | Present only while status is needs_review: the last position a person confirmed. |
isResolved |
boolean | true once the thread is resolved. Only set on the first comment of a thread. |
isDeleted |
boolean | true on a deleted first comment kept because it has replies. |
createdAt |
string (date-time) | When it was posted. |
updatedAt |
string (date-time) | When it last changed. |
authorName |
string | The author’s display name: the name a share-link viewer wrote as, or the team member’s name, or “Team member” when a member has none or has left the company. |
anchor – 9 more fields
| Field | Type | Description |
|---|---|---|
startWord |
integer | Position of the first word. |
endWord |
integer | Position of the last word, inclusive. |
exact |
string | The quoted words, joined by single spaces. |
prefix |
string | Up to 32 characters of transcript text before the quote. |
suffix |
string | Up to 32 characters of transcript text after the quote. |
startInSec |
number | Start time of the first word, in seconds. |
endInSec |
number | End time of the last word, in seconds. |
speakerIds |
string[] | Speakers of the quoted words. |
transcriptRevision |
integer | The transcript revision these positions refer to. |
lastResolved – 9 more fields
| Field | Type | Description |
|---|---|---|
startWord |
integer | Position of the first word. |
endWord |
integer | Position of the last word, inclusive. |
exact |
string | The quoted words, joined by single spaces. |
prefix |
string | Up to 32 characters of transcript text before the quote. |
suffix |
string | Up to 32 characters of transcript text after the quote. |
startInSec |
number | Start time of the first word, in seconds. |
endInSec |
number | End time of the last word, in seconds. |
speakerIds |
string[] | Speakers of the quoted words. |
transcriptRevision |
integer | The transcript revision these positions refer to. |
Example response (Success), application/json.
{
"status": "success",
"data": {
"commentId": "Cm8WpE3kLv6NdHa",
"mediaId": "b4994aa1267c",
"anchor": {
"startWord": 42,
"endWord": 49,
"exact": "we need the pricing sheet by Friday",
"prefix": "Before we close the quarter, ",
"suffix": ". Can you send it over?",
"startInSec": 61.4,
"endInSec": 64.2,
"speakerIds": [
"1"
],
"transcriptRevision": 3
},
"parentId": null,
"body": "Can we confirm this date with finance?",
"status": "active",
"isResolved": true,
"isDeleted": false,
"createdAt": "2026-10-06T14:05:12.000Z",
"updatedAt": "2026-10-06T16:30:00.000Z",
"resolvedBy": "65f0c1a2b3c4d5e6f7a8b9c0",
"authorName": "Reviewer A"
}
}400 The body broke a rule or failed validation
Response body, application/json.
| Field | Type | Description |
|---|---|---|
status |
string | failed. One of: failed . |
requestId |
string | Id of this request, for support. |
code |
integer | The HTTP status code again. |
message |
string | What went wrong, in words you can show to a person. |
hints |
string[] | Generic hints for this kind of error. |
Example response (The body broke a rule or failed validation), application/json.
{
"status": "failed",
"requestId": "req_0000000000000000",
"code": 400,
"message": "Only the first comment in a thread can be resolved.",
"hints": [
"The API request is invalid or improperly formed."
]
}404 Any refusal: unknown or revoked token, expired subscription, the setting is off, view mode on a write, media outside the link, a name that is not allowed, or someone else’s entry
Response body, application/json.
| Field | Type | Description |
|---|---|---|
status |
string | failed. One of: failed . |
requestId |
string | Id of this request, for support. |
code |
integer | The HTTP status code again. |
message |
string | What went wrong, in words you can show to a person. |
hints |
string[] | Generic hints for this kind of error. |
429 Rate limit reached
The spec records this status code with no example body.
Labels on a transcript
List labels through a share link
/embed/media/{mediaId}/labelsRead the labelled passages of a media file through a dashboard share link. Allowed while the dashboard’s labels.isEnabled is true. Only the labels the dashboard shows are returned (its label groups, or every active label when it lists none). No API key or access token: the share link token is the only credential. The media must be in one of the dashboard’s folders. Every refusal returns the same 404 {"message": "Not found."}, so a link holder cannot tell which check failed. A media or folder embed token also works here while the embed’s meta.isLabels is true, for the embed’s own media or media in its folders; an embed shows every active label and can never write. Reads are limited to 120 requests a minute per IP address.
Parameters
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
mediaId |
path | string | Yes | Id of the media file. Trimmed, then 1 to 64 characters from A-Z, a-z, 0-9, _ and - (pattern ^[A-Za-z0-9_-]{1,64}$). Anything else returns 400. |
token |
query | string | Yes | The share link token of a dashboard, or of a media or folder embed on the read routes. Up to 200 characters. |
curl -X GET 'https://api.speakai.co/v1/embed/media/b4994aa1267c/labels?token=dsh_3kQm9XpL2vRt'200 Success
Response body, application/json.
| Field | Type | Description |
|---|---|---|
status |
string | success on every 2xx response. One of: success . |
data |
object | The result. |
data.transcriptRevision |
integer | The media’s current transcript revision. Send it as expectedTranscriptRevision when you label or comment. |
data.labels |
object[] | Name, colour and description of every label used in mediaLabels. |
data.mediaLabels |
object[] | Labelled passages, each narrowed to the labels the link shows. A passage left with none is dropped. |
data.labels – 5 more fields
| Field | Type | Description |
|---|---|---|
labelId |
string | Id of the label. |
name |
string | Name of the label. |
color |
string | Hex colour, such as #6366f1. |
description |
string | Description of the label. |
parentId |
string,null | Id of the label’s group, or null. |
data.mediaLabels – 9 more fields
| Field | Type | Description |
|---|---|---|
mediaLabelId |
string | Id of the labelled passage. |
mediaId |
string | Id of the media. |
labelIds |
string[] | Labels on the passage. |
anchor |
object | Where the passage sits in the transcript. Built by the server from the word range you send. |
status |
string | active: the same words, possibly at new positions after an edit. shifted: most of the words survived an edit near the same time, but the text changed. needs_review: the words changed too much to follow; anchor then holds a suggested passage on the new transcript and lastResolved the last position a person confirmed. One of: active, shifted, needs_review . |
lastResolved |
object | Present only while status is needs_review: the last position a person confirmed. |
createdAt |
string (date-time) | When it was created. |
updatedAt |
string (date-time) | When it last changed. |
authorName |
string | The author’s display name: the name a share-link viewer wrote as, or the team member’s name, or “Team member” when a member has none or has left the company. |
anchor – 9 more fields
| Field | Type | Description |
|---|---|---|
startWord |
integer | Position of the first word. |
endWord |
integer | Position of the last word, inclusive. |
exact |
string | The quoted words, joined by single spaces. |
prefix |
string | Up to 32 characters of transcript text before the quote. |
suffix |
string | Up to 32 characters of transcript text after the quote. |
startInSec |
number | Start time of the first word, in seconds. |
endInSec |
number | End time of the last word, in seconds. |
speakerIds |
string[] | Speakers of the quoted words. |
transcriptRevision |
integer | The transcript revision these positions refer to. |
lastResolved – 9 more fields
| Field | Type | Description |
|---|---|---|
startWord |
integer | Position of the first word. |
endWord |
integer | Position of the last word, inclusive. |
exact |
string | The quoted words, joined by single spaces. |
prefix |
string | Up to 32 characters of transcript text before the quote. |
suffix |
string | Up to 32 characters of transcript text after the quote. |
startInSec |
number | Start time of the first word, in seconds. |
endInSec |
number | End time of the last word, in seconds. |
speakerIds |
string[] | Speakers of the quoted words. |
transcriptRevision |
integer | The transcript revision these positions refer to. |
Example response (Success), application/json.
{
"status": "success",
"data": {
"transcriptRevision": 3,
"mediaLabels": [
{
"mediaLabelId": "Ml5RtB7nZc2GyQs",
"mediaId": "b4994aa1267c",
"labelIds": [
"Lb3NcY8sDf4HqPe"
],
"anchor": {
"startWord": 42,
"endWord": 49,
"exact": "we need the pricing sheet by Friday",
"prefix": "Before we close the quarter, ",
"suffix": ". Can you send it over?",
"startInSec": 61.4,
"endInSec": 64.2,
"speakerIds": [
"1"
],
"transcriptRevision": 3
},
"status": "active",
"createdAt": "2026-10-06T14:05:12.000Z",
"updatedAt": "2026-10-06T14:05:12.000Z",
"authorName": "Reviewer A"
}
]
}
}400 A path or query value failed validation
Response body, application/json.
| Field | Type | Description |
|---|---|---|
status |
string | failed. One of: failed . |
requestId |
string | Id of this request, for support. |
code |
integer | The HTTP status code again. |
message |
string | What went wrong, in words you can show to a person. |
hints |
string[] | Generic hints for this kind of error. |
Example response (A path or query value failed validation), application/json.
{
"status": "failed",
"requestId": "req_0000000000000000",
"code": 400,
"message": "\"status\" must be one of [active, shifted, needs_review]",
"hints": [
"The API request is invalid or improperly formed."
]
}404 Any refusal: unknown or revoked token, expired subscription, the setting is off, view mode on a write, media outside the link, a name that is not allowed, or someone else’s entry
Response body, application/json.
| Field | Type | Description |
|---|---|---|
status |
string | failed. One of: failed . |
requestId |
string | Id of this request, for support. |
code |
integer | The HTTP status code again. |
message |
string | What went wrong, in words you can show to a person. |
hints |
string[] | Generic hints for this kind of error. |
429 Rate limit reached
The spec records this status code with no example body.
Label a passage through a share link
/embed/media/{mediaId}/labelsLabel a passage through a dashboard share link. Allowed while labels are on with labels.mode set to apply. Word positions and expectedTranscriptRevision work as on POST /media/{mediaId}/labels, and a stale revision returns 409. Every label must be one the dashboard offers, or the request returns 400 “Choose labels this dashboard offers.” Adding labels to the same words merges only into your own passage from this dashboard. No API key or access token: the share link token is the only credential. The media must be in one of the dashboard’s folders. Every refusal returns the same 404 {"message": "Not found."}, so a link holder cannot tell which check failed. The entry is recorded under reviewerName via this dashboard. The name is chosen by the link holder and not authenticated, the same as feedback; only a name the dashboard allows is accepted. A name can change or delete only the entries written under it from the same dashboard. Writes are limited to 30 requests a minute per IP address.
Parameters
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
mediaId |
path | string | Yes | Id of the media file. Trimmed, then 1 to 64 characters from A-Z, a-z, 0-9, _ and - (pattern ^[A-Za-z0-9_-]{1,64}$). Anything else returns 400. |
token |
query | string | Yes | The share link token of a dashboard, or of a media or folder embed on the read routes. Up to 200 characters. |
Request body
Fields marked required are the ones the server rejects the request without. Anything conditional, where a field becomes required only alongside another, is described under Request rules above rather than marked here.
| Field | Type | Description |
|---|---|---|
reviewerName |
string, required | The name you write as: one of annotationSettings.reviewers, or any name when allowOtherReviewer is true. Chosen by the link holder and not authenticated. Trimmed; a listed name in another case is stored with the listed spelling. Required. |
range |
object, required | Inclusive range of word positions. Word positions count every word of the transcript in order, starting at 0. Within each sentence, words come from entities in order (an entity that holds several words, such as “New York”, counts as several), or from the sentence text split on whitespace when the sentence has no entities. A token that is only punctuation is not counted. |
range.start |
integer, required | Position of the first word. Integer, 0 or more. |
range.end |
integer, required | Position of the last word, inclusive. Integer, at least start. A range that runs past the last word returns 400 “The selected words are outside this transcript.” |
labelIds |
string[], required | Labels to put on the passage. 1 to 20 ids, no repeats. Each must be an active label (not a group) in your company, or the request returns 400 “Choose active labels from this workspace (groups cannot be applied).” |
expectedTranscriptRevision |
integer, required | The transcriptRevision you read the transcript at. Integer, 0 or more. A stale value returns 409. |
curl -X POST 'https://api.speakai.co/v1/embed/media/b4994aa1267c/labels?token=dsh_3kQm9XpL2vRt' \
-H 'Content-Type: application/json' \
-d '{
"reviewerName": "Reviewer A",
"range": {
"start": 42,
"end": 49
},
"labelIds": [
"Lb3NcY8sDf4HqPe"
],
"expectedTranscriptRevision": 3
}'200 Success
Response body, application/json.
| Field | Type | Description |
|---|---|---|
status |
string | success on every 2xx response. One of: success . |
data |
object | Labels on one passage of a transcript. |
data – 9 more fields
| Field | Type | Description |
|---|---|---|
mediaLabelId |
string | Id of the labelled passage. |
mediaId |
string | Id of the media. |
labelIds |
string[] | Labels on the passage. |
anchor |
object | Where the passage sits in the transcript. Built by the server from the word range you send. |
status |
string | active: the same words, possibly at new positions after an edit. shifted: most of the words survived an edit near the same time, but the text changed. needs_review: the words changed too much to follow; anchor then holds a suggested passage on the new transcript and lastResolved the last position a person confirmed. One of: active, shifted, needs_review . |
lastResolved |
object | Present only while status is needs_review: the last position a person confirmed. |
createdAt |
string (date-time) | When it was created. |
updatedAt |
string (date-time) | When it last changed. |
authorName |
string | The author’s display name: the name a share-link viewer wrote as, or the team member’s name, or “Team member” when a member has none or has left the company. |
anchor – 9 more fields
| Field | Type | Description |
|---|---|---|
startWord |
integer | Position of the first word. |
endWord |
integer | Position of the last word, inclusive. |
exact |
string | The quoted words, joined by single spaces. |
prefix |
string | Up to 32 characters of transcript text before the quote. |
suffix |
string | Up to 32 characters of transcript text after the quote. |
startInSec |
number | Start time of the first word, in seconds. |
endInSec |
number | End time of the last word, in seconds. |
speakerIds |
string[] | Speakers of the quoted words. |
transcriptRevision |
integer | The transcript revision these positions refer to. |
lastResolved – 9 more fields
| Field | Type | Description |
|---|---|---|
startWord |
integer | Position of the first word. |
endWord |
integer | Position of the last word, inclusive. |
exact |
string | The quoted words, joined by single spaces. |
prefix |
string | Up to 32 characters of transcript text before the quote. |
suffix |
string | Up to 32 characters of transcript text after the quote. |
startInSec |
number | Start time of the first word, in seconds. |
endInSec |
number | End time of the last word, in seconds. |
speakerIds |
string[] | Speakers of the quoted words. |
transcriptRevision |
integer | The transcript revision these positions refer to. |
Example response (Success), application/json.
{
"status": "success",
"data": {
"mediaLabelId": "Ml5RtB7nZc2GyQs",
"mediaId": "b4994aa1267c",
"labelIds": [
"Lb3NcY8sDf4HqPe"
],
"anchor": {
"startWord": 42,
"endWord": 49,
"exact": "we need the pricing sheet by Friday",
"prefix": "Before we close the quarter, ",
"suffix": ". Can you send it over?",
"startInSec": 61.4,
"endInSec": 64.2,
"speakerIds": [
"1"
],
"transcriptRevision": 3
},
"status": "active",
"createdAt": "2026-10-06T14:05:12.000Z",
"updatedAt": "2026-10-06T14:05:12.000Z",
"authorName": "Reviewer A"
}
}400 Validation, range, label or limit problem
Response body, application/json.
| Field | Type | Description |
|---|---|---|
status |
string | failed. One of: failed . |
requestId |
string | Id of this request, for support. |
code |
integer | The HTTP status code again. |
message |
string | What went wrong, in words you can show to a person. |
hints |
string[] | Generic hints for this kind of error. |
Example response (Validation, range, label or limit problem), application/json.
{
"status": "failed",
"requestId": "req_0000000000000000",
"code": 400,
"message": "The selected words are outside this transcript.",
"hints": [
"The API request is invalid or improperly formed."
]
}404 Any refusal: unknown or revoked token, expired subscription, the setting is off, view mode on a write, media outside the link, a name that is not allowed, or someone else’s entry
Response body, application/json.
| Field | Type | Description |
|---|---|---|
status |
string | failed. One of: failed . |
requestId |
string | Id of this request, for support. |
code |
integer | The HTTP status code again. |
message |
string | What went wrong, in words you can show to a person. |
hints |
string[] | Generic hints for this kind of error. |
409 The transcript changed since you read it
Response body, application/json.
| Field | Type | Description |
|---|---|---|
status |
string | failed. One of: failed . |
requestId |
string | Id of this request, for support. |
code |
integer | The HTTP status code again. |
message |
string | What went wrong, in words you can show to a person. |
hints |
string[] | Generic hints for this kind of error. |
Example response (The transcript changed since you read it), application/json.
{
"status": "failed",
"requestId": "req_0000000000000000",
"code": 409,
"message": "This transcript changed since you opened it. Reload to keep editing.",
"hints": [
"Please use PUT for update instead of POST",
"The requested operation failed because it tried to create a resource that already exists."
]
}429 Rate limit reached
The spec records this status code with no example body.
Change a passage’s labels through a share link
/embed/media/{mediaId}/labels/{mediaLabelId}Replace the labels on your own passage through a dashboard share link. Allowed while labels are on with labels.mode set to apply. Every label must be one the dashboard offers, or the request returns 400 “Choose labels this dashboard offers.” Moving a passage and reviewing re-anchored passages stay in the app. No API key or access token: the share link token is the only credential. The media must be in one of the dashboard’s folders. Every refusal returns the same 404 {"message": "Not found."}, so a link holder cannot tell which check failed. The entry is recorded under reviewerName via this dashboard. The name is chosen by the link holder and not authenticated, the same as feedback; only a name the dashboard allows is accepted. A name can change or delete only the entries written under it from the same dashboard. Writes are limited to 30 requests a minute per IP address.
Parameters
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
mediaId |
path | string | Yes | Id of the media file. Trimmed, then 1 to 64 characters from A-Z, a-z, 0-9, _ and - (pattern ^[A-Za-z0-9_-]{1,64}$). Anything else returns 400. |
mediaLabelId |
path | string | Yes | Id of the labelled passage, from mediaLabelId. Trimmed, then 1 to 64 characters from A-Z, a-z, 0-9, _ and - (pattern ^[A-Za-z0-9_-]{1,64}$). Anything else returns 400. |
token |
query | string | Yes | The share link token of a dashboard, or of a media or folder embed on the read routes. Up to 200 characters. |
Request body
Fields marked required are the ones the server rejects the request without. Anything conditional, where a field becomes required only alongside another, is described under Request rules above rather than marked here.
| Field | Type | Description |
|---|---|---|
reviewerName |
string, required | The name you write as: one of annotationSettings.reviewers, or any name when allowOtherReviewer is true. Chosen by the link holder and not authenticated. Trimmed; a listed name in another case is stored with the listed spelling. Required. |
labelIds |
string[], required | Labels to put on the passage. 1 to 20 ids, no repeats. Each must be an active label (not a group) in your company, or the request returns 400 “Choose active labels from this workspace (groups cannot be applied).” |
curl -X PATCH 'https://api.speakai.co/v1/embed/media/b4994aa1267c/labels/Ml5RtB7nZc2GyQs?token=dsh_3kQm9XpL2vRt' \
-H 'Content-Type: application/json' \
-d '{
"reviewerName": "Reviewer A",
"labelIds": [
"Lb3NcY8sDf4HqPe"
]
}'200 Success
Response body, application/json.
| Field | Type | Description |
|---|---|---|
status |
string | success on every 2xx response. One of: success . |
data |
object | Labels on one passage of a transcript. |
data – 9 more fields
| Field | Type | Description |
|---|---|---|
mediaLabelId |
string | Id of the labelled passage. |
mediaId |
string | Id of the media. |
labelIds |
string[] | Labels on the passage. |
anchor |
object | Where the passage sits in the transcript. Built by the server from the word range you send. |
status |
string | active: the same words, possibly at new positions after an edit. shifted: most of the words survived an edit near the same time, but the text changed. needs_review: the words changed too much to follow; anchor then holds a suggested passage on the new transcript and lastResolved the last position a person confirmed. One of: active, shifted, needs_review . |
lastResolved |
object | Present only while status is needs_review: the last position a person confirmed. |
createdAt |
string (date-time) | When it was created. |
updatedAt |
string (date-time) | When it last changed. |
authorName |
string | The author’s display name: the name a share-link viewer wrote as, or the team member’s name, or “Team member” when a member has none or has left the company. |
anchor – 9 more fields
| Field | Type | Description |
|---|---|---|
startWord |
integer | Position of the first word. |
endWord |
integer | Position of the last word, inclusive. |
exact |
string | The quoted words, joined by single spaces. |
prefix |
string | Up to 32 characters of transcript text before the quote. |
suffix |
string | Up to 32 characters of transcript text after the quote. |
startInSec |
number | Start time of the first word, in seconds. |
endInSec |
number | End time of the last word, in seconds. |
speakerIds |
string[] | Speakers of the quoted words. |
transcriptRevision |
integer | The transcript revision these positions refer to. |
lastResolved – 9 more fields
| Field | Type | Description |
|---|---|---|
startWord |
integer | Position of the first word. |
endWord |
integer | Position of the last word, inclusive. |
exact |
string | The quoted words, joined by single spaces. |
prefix |
string | Up to 32 characters of transcript text before the quote. |
suffix |
string | Up to 32 characters of transcript text after the quote. |
startInSec |
number | Start time of the first word, in seconds. |
endInSec |
number | End time of the last word, in seconds. |
speakerIds |
string[] | Speakers of the quoted words. |
transcriptRevision |
integer | The transcript revision these positions refer to. |
Example response (Success), application/json.
{
"status": "success",
"data": {
"mediaLabelId": "Ml5RtB7nZc2GyQs",
"mediaId": "b4994aa1267c",
"labelIds": [
"Lb3NcY8sDf4HqPe",
"Lb9TmK2vXa6JrWo"
],
"anchor": {
"startWord": 42,
"endWord": 49,
"exact": "we need the pricing sheet by Friday",
"prefix": "Before we close the quarter, ",
"suffix": ". Can you send it over?",
"startInSec": 61.4,
"endInSec": 64.2,
"speakerIds": [
"1"
],
"transcriptRevision": 3
},
"status": "active",
"createdAt": "2026-10-06T14:05:12.000Z",
"updatedAt": "2026-10-06T14:05:12.000Z",
"authorName": "Reviewer A"
}
}400 The body broke a rule or failed validation
Response body, application/json.
| Field | Type | Description |
|---|---|---|
status |
string | failed. One of: failed . |
requestId |
string | Id of this request, for support. |
code |
integer | The HTTP status code again. |
message |
string | What went wrong, in words you can show to a person. |
hints |
string[] | Generic hints for this kind of error. |
Example response (The body broke a rule or failed validation), application/json.
{
"status": "failed",
"requestId": "req_0000000000000000",
"code": 400,
"message": "\"value\" must contain at least one of [labelIds, action]",
"hints": [
"The API request is invalid or improperly formed."
]
}404 Any refusal: unknown or revoked token, expired subscription, the setting is off, view mode on a write, media outside the link, a name that is not allowed, or someone else’s entry
Response body, application/json.
| Field | Type | Description |
|---|---|---|
status |
string | failed. One of: failed . |
requestId |
string | Id of this request, for support. |
code |
integer | The HTTP status code again. |
message |
string | What went wrong, in words you can show to a person. |
hints |
string[] | Generic hints for this kind of error. |
409 The transcript changed since you read it
Response body, application/json.
| Field | Type | Description |
|---|---|---|
status |
string | failed. One of: failed . |
requestId |
string | Id of this request, for support. |
code |
integer | The HTTP status code again. |
message |
string | What went wrong, in words you can show to a person. |
hints |
string[] | Generic hints for this kind of error. |
Example response (The transcript changed since you read it), application/json.
{
"status": "failed",
"requestId": "req_0000000000000000",
"code": 409,
"message": "This transcript changed since you opened it. Reload to keep editing.",
"hints": [
"Please use PUT for update instead of POST",
"The requested operation failed because it tried to create a resource that already exists."
]
}429 Rate limit reached
The spec records this status code with no example body.
Remove a passage through a share link
/embed/media/{mediaId}/labels/{mediaLabelId}Remove your own labelled passage through a dashboard share link. Allowed while labels are on with labels.mode set to apply. No API key or access token: the share link token is the only credential. The media must be in one of the dashboard’s folders. Every refusal returns the same 404 {"message": "Not found."}, so a link holder cannot tell which check failed. The entry is recorded under reviewerName via this dashboard. The name is chosen by the link holder and not authenticated, the same as feedback; only a name the dashboard allows is accepted. A name can change or delete only the entries written under it from the same dashboard. Writes are limited to 30 requests a minute per IP address.
Parameters
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
mediaId |
path | string | Yes | Id of the media file. Trimmed, then 1 to 64 characters from A-Z, a-z, 0-9, _ and - (pattern ^[A-Za-z0-9_-]{1,64}$). Anything else returns 400. |
mediaLabelId |
path | string | Yes | Id of the labelled passage, from mediaLabelId. Trimmed, then 1 to 64 characters from A-Z, a-z, 0-9, _ and - (pattern ^[A-Za-z0-9_-]{1,64}$). Anything else returns 400. |
token |
query | string | Yes | The share link token of a dashboard, or of a media or folder embed on the read routes. Up to 200 characters. |
Request body
Fields marked required are the ones the server rejects the request without. Anything conditional, where a field becomes required only alongside another, is described under Request rules above rather than marked here.
| Field | Type | Description |
|---|---|---|
reviewerName |
string, required | The name you write as: one of annotationSettings.reviewers, or any name when allowOtherReviewer is true. Chosen by the link holder and not authenticated. Trimmed; a listed name in another case is stored with the listed spelling. Required. |
curl -X DELETE 'https://api.speakai.co/v1/embed/media/b4994aa1267c/labels/Ml5RtB7nZc2GyQs?token=dsh_3kQm9XpL2vRt' \
-H 'Content-Type: application/json' \
-d '{
"reviewerName": "Reviewer A"
}'200 Success
Response body, application/json.
| Field | Type | Description |
|---|---|---|
status |
string | success on every 2xx response. One of: success . |
data |
object | The result. |
data.mediaLabelId |
string | Id of the passage you removed. |
Example response (Success), application/json.
{
"status": "success",
"data": {
"mediaLabelId": "Ml5RtB7nZc2GyQs"
}
}404 Any refusal: unknown or revoked token, expired subscription, the setting is off, view mode on a write, media outside the link, a name that is not allowed, or someone else’s entry
Response body, application/json.
| Field | Type | Description |
|---|---|---|
status |
string | failed. One of: failed . |
requestId |
string | Id of this request, for support. |
code |
integer | The HTTP status code again. |
message |
string | What went wrong, in words you can show to a person. |
hints |
string[] | Generic hints for this kind of error. |
429 Rate limit reached
The spec records this status code with no example body.
Related pages
- API reference for the base URL, authentication, and the error format.
- Authenticate with the Speak AI API using access tokens
- Upload audio and video to Speak AI and read insights
- Create and update live transcription sessions
- Analyze text notes with the Speak AI text endpoints
Get an API key on the Speak AI developer page.