---
title: "Labels and comments through share links"
description: "Read labels and comments through a dashboard share link or embed token, and let listed reviewers label passages and comment without signing in."
---

> Documentation Index
> Fetch the complete documentation index at: https://docs.speakai.co/llms.txt
> Use this file to discover all available pages before exploring further.

# Labels and comments through share links


import EndpointIndex from "@/components/api/EndpointIndex.astro";

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.

| Method | Path | What it does |
| --- | --- | --- |
| `GET` | [`/embed/insight`](#get-embed-insight) | Read a shared media page |
| `DELETE` | [`/embed/media/{mediaId}/comments/{commentId}`](#delete-embed-media-media-id-comments-comment-id) | Delete a comment through a share link |
| `GET` | [`/embed/dashboards/config`](#get-embed-dashboards-config) | Read a shared dashboard's layout and annotation settings |
| `GET` | [`/embed/media/{mediaId}/comments`](#get-embed-media-media-id-comments) | List comments through a share link |
| `POST` | [`/embed/media/{mediaId}/comments`](#post-embed-media-media-id-comments) | Comment through a share link |
| `PATCH` | [`/embed/media/{mediaId}/comments/{commentId}`](#patch-embed-media-media-id-comments-comment-id) | Edit a comment through a share link |

### Labels on a transcript

| Method | Path | What it does |
| --- | --- | --- |
| `GET` | [`/embed/media/{mediaId}/labels`](#get-embed-media-media-id-labels) | List labels through a share link |
| `POST` | [`/embed/media/{mediaId}/labels`](#post-embed-media-media-id-labels) | Label a passage through a share link |
| `PATCH` | [`/embed/media/{mediaId}/labels/{mediaLabelId}`](#patch-embed-media-media-id-labels-media-label-id) | Change a passage's labels through a share link |
| `DELETE` | [`/embed/media/{mediaId}/labels/{mediaLabelId}`](#delete-embed-media-media-id-labels-media-label-id) | Remove a passage through a share link |

<h2 id="get-embed-insight">Read a shared media page</h2>

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

<div slot="code">

<CodePanel label="Example request">

```bash
curl -X GET 'https://api.speakai.co/v1/embed/insight?token=dsh_3kQm9XpL2vRt&mediaId=b4994aa1267c'
```

</CodePanel>

<ResponsePanel id="get-embed-insight" statuses={[{"code":"200","description":"Success"},{"code":"403","description":"The media is not in the dashboard's folders"},{"code":"404","description":"Unknown token or no active subscription"}]}>

<div slot="status-200">

**`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. |

<details>
<summary>`data.mediaData` -- 2 more fields</summary>

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

</details>

<details>
<summary>`data.annotationSettings` -- 4 more fields</summary>

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

<details>
<summary>`labels.groups` -- 2 more fields</summary>

| Field | Type |
| --- | --- |
| `labelId` | string |
| `name` | string |

</details>

<details>
<summary>`labels.labels` -- 5 more fields</summary>

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

</details>

</details>

</div>

<div slot="status-403">

**`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. |

</div>

<div slot="status-404">

**`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. |

</div>

</ResponsePanel>

</div>

<h2 id="delete-embed-media-media-id-comments-comment-id">Delete a comment through a share link</h2>

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

<div slot="code">

<CodePanel label="Example request">

```bash
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"
}'
```

</CodePanel>

<ResponsePanel id="delete-embed-media-media-id-comments-comment-id" statuses={[{"code":"200","description":"Success"},{"code":"404","description":"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"},{"code":"429","description":"Rate limit reached"}]}>

<div slot="status-200">

**`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`.

```json
{
  "status": "success",
  "data": {
"commentId": "Cm8WpE3kLv6NdHa"
  }
}
```

</div>

<div slot="status-404">

**`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. |

</div>

<div slot="status-429">

**`429` Rate limit reached**

The spec records this status code with no example body.

</div>

</ResponsePanel>

</div>

<h2 id="get-embed-dashboards-config">Read a shared dashboard's layout and annotation settings</h2>

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

<div slot="code">

<CodePanel label="Example request">

```bash
curl -X GET 'https://api.speakai.co/v1/embed/dashboards/config?token=dsh_3kQm9XpL2vRt'
```

</CodePanel>

<ResponsePanel id="get-embed-dashboards-config" statuses={[{"code":"200","description":"Success"},{"code":"404","description":"Unknown or revoked token"}]}>

<div slot="status-200">

**`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. |

<details>
<summary>`data.annotationSettings` -- 4 more fields</summary>

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

<details>
<summary>`labels.groups` -- 2 more fields</summary>

| Field | Type |
| --- | --- |
| `labelId` | string |
| `name` | string |

</details>

<details>
<summary>`labels.labels` -- 5 more fields</summary>

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

</details>

</details>

</div>

<div slot="status-404">

**`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. |

</div>

</ResponsePanel>

</div>

<h2 id="get-embed-media-media-id-comments">List comments through a share link</h2>

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

<div slot="code">

<CodePanel label="Example request">

```bash
curl -X GET 'https://api.speakai.co/v1/embed/media/b4994aa1267c/comments?token=dsh_3kQm9XpL2vRt'
```

</CodePanel>

<ResponsePanel id="get-embed-media-media-id-comments" statuses={[{"code":"200","description":"Success"},{"code":"400","description":"A path or query value failed validation"},{"code":"404","description":"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"},{"code":"429","description":"Rate limit reached"}]}>

<div slot="status-200">

**`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. |

<details>
<summary>`data.threads` -- 13 more fields</summary>

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

<details>
<summary>`anchor` -- 9 more fields</summary>

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

</details>

<details>
<summary>`lastResolved` -- 9 more fields</summary>

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

</details>

<details>
<summary>`replies` -- 12 more fields</summary>

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

<details>
<summary>`anchor` -- 9 more fields</summary>

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

</details>

<details>
<summary>`lastResolved` -- 9 more fields</summary>

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

</details>

</details>

</details>

Example response (Success), `application/json`.

```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"
  }
]
  }
}
```

</div>

<div slot="status-400">

**`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`.

```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."
  ]
}
```

</div>

<div slot="status-404">

**`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. |

</div>

<div slot="status-429">

**`429` Rate limit reached**

The spec records this status code with no example body.

</div>

</ResponsePanel>

</div>

<h2 id="post-embed-media-media-id-comments">Comment through a share link</h2>

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

<div slot="code">

<CodePanel label="Example request">

```bash
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
}'
```

</CodePanel>

<ResponsePanel id="post-embed-media-media-id-comments" statuses={[{"code":"200","description":"Success"},{"code":"400","description":"Validation, reply depth, range or limit problem"},{"code":"404","description":"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"},{"code":"409","description":"The transcript changed since you read it"},{"code":"429","description":"Rate limit reached"}]}>

<div slot="status-200">

**`200` Success**

Response body, `application/json`.

| Field | Type | Description |
| --- | --- | --- |
| `status` | string | `success` on every 2xx response. One of: `success` . |
| `data` | object | A comment. |

<details>
<summary>`data` -- 12 more fields</summary>

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

<details>
<summary>`anchor` -- 9 more fields</summary>

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

</details>

<details>
<summary>`lastResolved` -- 9 more fields</summary>

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

</details>

</details>

Example response (Success), `application/json`.

```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"
  }
}
```

</div>

<div slot="status-400">

**`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`.

```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."
  ]
}
```

</div>

<div slot="status-404">

**`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. |

</div>

<div slot="status-409">

**`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`.

```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."
  ]
}
```

</div>

<div slot="status-429">

**`429` Rate limit reached**

The spec records this status code with no example body.

</div>

</ResponsePanel>

</div>

<h2 id="patch-embed-media-media-id-comments-comment-id">Edit a comment through a share link</h2>

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

<div slot="code">

<CodePanel label="Example request">

```bash
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."
}'
```

</CodePanel>

<ResponsePanel id="patch-embed-media-media-id-comments-comment-id" statuses={[{"code":"200","description":"Success"},{"code":"400","description":"The body broke a rule or failed validation"},{"code":"404","description":"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"},{"code":"429","description":"Rate limit reached"}]}>

<div slot="status-200">

**`200` Success**

Response body, `application/json`.

| Field | Type | Description |
| --- | --- | --- |
| `status` | string | `success` on every 2xx response. One of: `success` . |
| `data` | object | A comment. |

<details>
<summary>`data` -- 12 more fields</summary>

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

<details>
<summary>`anchor` -- 9 more fields</summary>

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

</details>

<details>
<summary>`lastResolved` -- 9 more fields</summary>

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

</details>

</details>

Example response (Success), `application/json`.

```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"
  }
}
```

</div>

<div slot="status-400">

**`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`.

```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."
  ]
}
```

</div>

<div slot="status-404">

**`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. |

</div>

<div slot="status-429">

**`429` Rate limit reached**

The spec records this status code with no example body.

</div>

</ResponsePanel>

</div>

## Labels on a transcript

<h2 id="get-embed-media-media-id-labels">List labels through a share link</h2>

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

<div slot="code">

<CodePanel label="Example request">

```bash
curl -X GET 'https://api.speakai.co/v1/embed/media/b4994aa1267c/labels?token=dsh_3kQm9XpL2vRt'
```

</CodePanel>

<ResponsePanel id="get-embed-media-media-id-labels" statuses={[{"code":"200","description":"Success"},{"code":"400","description":"A path or query value failed validation"},{"code":"404","description":"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"},{"code":"429","description":"Rate limit reached"}]}>

<div slot="status-200">

**`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. |

<details>
<summary>`data.labels` -- 5 more fields</summary>

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

</details>

<details>
<summary>`data.mediaLabels` -- 9 more fields</summary>

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

<details>
<summary>`anchor` -- 9 more fields</summary>

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

</details>

<details>
<summary>`lastResolved` -- 9 more fields</summary>

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

</details>

</details>

Example response (Success), `application/json`.

```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"
  }
]
  }
}
```

</div>

<div slot="status-400">

**`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`.

```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."
  ]
}
```

</div>

<div slot="status-404">

**`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. |

</div>

<div slot="status-429">

**`429` Rate limit reached**

The spec records this status code with no example body.

</div>

</ResponsePanel>

</div>

<h2 id="post-embed-media-media-id-labels">Label a passage through a share link</h2>

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

<div slot="code">

<CodePanel label="Example request">

```bash
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
}'
```

</CodePanel>

<ResponsePanel id="post-embed-media-media-id-labels" statuses={[{"code":"200","description":"Success"},{"code":"400","description":"Validation, range, label or limit problem"},{"code":"404","description":"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"},{"code":"409","description":"The transcript changed since you read it"},{"code":"429","description":"Rate limit reached"}]}>

<div slot="status-200">

**`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. |

<details>
<summary>`data` -- 9 more fields</summary>

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

<details>
<summary>`anchor` -- 9 more fields</summary>

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

</details>

<details>
<summary>`lastResolved` -- 9 more fields</summary>

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

</details>

</details>

Example response (Success), `application/json`.

```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"
  }
}
```

</div>

<div slot="status-400">

**`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`.

```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."
  ]
}
```

</div>

<div slot="status-404">

**`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. |

</div>

<div slot="status-409">

**`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`.

```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."
  ]
}
```

</div>

<div slot="status-429">

**`429` Rate limit reached**

The spec records this status code with no example body.

</div>

</ResponsePanel>

</div>

<h2 id="patch-embed-media-media-id-labels-media-label-id">Change a passage's labels through a share link</h2>

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)." |

<div slot="code">

<CodePanel label="Example request">

```bash
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"
  ]
}'
```

</CodePanel>

<ResponsePanel id="patch-embed-media-media-id-labels-media-label-id" statuses={[{"code":"200","description":"Success"},{"code":"400","description":"The body broke a rule or failed validation"},{"code":"404","description":"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"},{"code":"409","description":"The transcript changed since you read it"},{"code":"429","description":"Rate limit reached"}]}>

<div slot="status-200">

**`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. |

<details>
<summary>`data` -- 9 more fields</summary>

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

<details>
<summary>`anchor` -- 9 more fields</summary>

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

</details>

<details>
<summary>`lastResolved` -- 9 more fields</summary>

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

</details>

</details>

Example response (Success), `application/json`.

```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"
  }
}
```

</div>

<div slot="status-400">

**`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`.

```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."
  ]
}
```

</div>

<div slot="status-404">

**`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. |

</div>

<div slot="status-409">

**`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`.

```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."
  ]
}
```

</div>

<div slot="status-429">

**`429` Rate limit reached**

The spec records this status code with no example body.

</div>

</ResponsePanel>

</div>

<h2 id="delete-embed-media-media-id-labels-media-label-id">Remove a passage through a share link</h2>

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

<div slot="code">

<CodePanel label="Example request">

```bash
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"
}'
```

</CodePanel>

<ResponsePanel id="delete-embed-media-media-id-labels-media-label-id" statuses={[{"code":"200","description":"Success"},{"code":"404","description":"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"},{"code":"429","description":"Rate limit reached"}]}>

<div slot="status-200">

**`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`.

```json
{
  "status": "success",
  "data": {
"mediaLabelId": "Ml5RtB7nZc2GyQs"
  }
}
```

</div>

<div slot="status-404">

**`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. |

</div>

<div slot="status-429">

**`429` Rate limit reached**

The spec records this status code with no example body.

</div>

</ResponsePanel>

</div>

## Related pages

- [API reference](/api/) for the base URL, authentication, and the error format.
- [Authenticate with the Speak AI API using access tokens](/api/authentication/)
- [Upload audio and video to Speak AI and read insights](/api/media/)
- [Create and update live transcription sessions](/api/live-transcription/)
- [Analyze text notes with the Speak AI text endpoints](/api/text/)

Get an API key on the [Speak AI developer page](https://app.speakai.co/developers?utm_source=docs&utm_medium=referral&utm_campaign=api-reference&utm_content=api-shared-links).

Source: https://docs.speakai.co/api/shared-links/index.mdx
