---
title: "Comment on transcripts with the comments API"
description: "Post comment threads on a passage of a Speak AI transcript or on the whole file, reply, resolve and reopen threads, and edit or delete comments."
---

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

# Comment on transcripts with the comments API


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

The Speak AI API exposes 4 comments endpoints under the base URL `https://api.speakai.co/v1`. Every request needs the `x-speakai-key` and `x-access-token` headers described in [Authentication](/api/authentication/).

Comment threads on a passage of a transcript or on the whole file, with one level of replies, resolve and reopen. Comments on passages follow their words when the transcript is edited, the same way labels do.

## Permissions

Admins set these per person in Team management. Owners pass every check. Reading comments needs no permission, and you can always edit and delete your own comments.

| Permission | Allows | Admin default | Member default |
| --- | --- | --- | --- |
| Comments: Create | Start a thread and reply | Yes | Yes |
| Comments: Update | Resolve and reopen threads | Yes | Yes |
| Comments: Delete | Delete other people's comments | Yes | No |

## Notifications

Every comment change is listed for the person who made it under the `comment` category of [GET /v1/notification](/api/notifications/). The person who started a thread is also told when someone else replies to it, resolves it or reopens it.

## What can you do with the comments endpoints?

Speak AI groups these 4 endpoints under the comments resource. Each entry below links to the full reference for that endpoint further down this page.

| Method | Path | What it does |
| --- | --- | --- |
| `GET` | [`/media/{mediaId}/comments`](#get-media-media-id-comments) | List comment threads |
| `POST` | [`/media/{mediaId}/comments`](#post-media-media-id-comments) | Post a comment or reply |
| `PATCH` | [`/media/{mediaId}/comments/{commentId}`](#patch-media-media-id-comments-comment-id) | Edit, resolve or reopen a comment |
| `DELETE` | [`/media/{mediaId}/comments/{commentId}`](#delete-media-media-id-comments-comment-id) | Delete a comment |

<h2 id="get-media-media-id-comments">List comment threads</h2>

Returns the media's comment threads, oldest first, with the media's current `transcriptRevision`. Each thread is its first comment with a `replies` array. Anyone who can open the file can read its comments; no comments permission is needed. The media must belong to your company and be one you can open. Owners and members with the folder **Access all** permission can open any file; everyone else can open files in folders assigned to them or their groups, and files they own. Any other media returns 404, the same as a media id that does not exist.

A deleted first comment that still has replies stays in the list with an empty `body` and `isDeleted: true`, so the replies keep their context. Deleted replies are left out.

When a transcript is saved, labels and comments on passages are moved to the new text in the background, after the save has answered. Until that finishes, which is normally a moment, a list can return anchors whose `anchor.transcriptRevision` is lower than the top-level `transcriptRevision`. Fetch the list again shortly after a save.

### 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. |
| `filter` | query | string | No | `all` (the default), `open` or `resolved` threads, or `file` for comments on the whole file only. One of: `all`, `open`, `resolved`, `file`. |

<div slot="code">

<CodePanel label="Example request">

```bash
curl -X GET 'https://api.speakai.co/v1/media/b4994aa1267c/comments?filter=open' \
  -H 'x-speakai-key: sk_test_speak_0000000000000000' \
  -H 'x-access-token: eyJhbGciOiJIUzI1NiJ9.test-access-token.0000000000'
```

</CodePanel>

<ResponsePanel id="get-media-media-id-comments" statuses={[{"code":"200","description":"Success"},{"code":"400","description":"A path or query value failed validation"},{"code":"404","description":"The media does not exist or you cannot open it"}]}>

<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` -- 15 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. |
| `mediaLabelId` | string | Labelled passage the comment is linked to. Present only when one was linked. |
| `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. |
| `resolvedBy` | string | Id of the person who resolved the thread. Present only while it is resolved. |
| `isDeleted` | boolean | `true` on a deleted first comment kept because it has replies. |
| `userId` | string | Id of the author. |
| `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. |

<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` -- 14 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. |
| `mediaLabelId` | string | Labelled passage the comment is linked to. Present only when one was linked. |
| `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. |
| `resolvedBy` | string | Id of the person who resolved the thread. Present only while it is resolved. |
| `isDeleted` | boolean | `true` on a deleted first comment kept because it has replies. |
| `userId` | string | Id of the author. |
| `createdAt` | string (date-time) | When it was posted. |
| `updatedAt` | string (date-time) | When it last changed. |

<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,
    "userId": "65f0c1a2b3c4d5e6f7a8b9c0",
    "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,
        "userId": "65f0c1a2b3c4d5e6f7a8b9c1",
        "createdAt": "2026-10-06T15:20:40.000Z",
        "updatedAt": "2026-10-06T15:20:40.000Z"
      }
    ]
  }
]
  }
}
```

</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` The media does not exist or you cannot open 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 media does not exist or you cannot open it), `application/json`.

```json
{
  "status": "failed",
  "requestId": "req_0000000000000000",
  "code": 404,
  "message": "Media not found.",
  "hints": [
"The requested operation failed because a resource associated with the request could not be found."
  ]
}
```

</div>

</ResponsePanel>

</div>

<h2 id="post-media-media-id-comments">Post a comment or reply</h2>

Requires the **Comments: Create** permission (on by default for admins and members). The media must belong to your company and be one you can open. Owners and members with the folder **Access all** permission can open any file; everyone else can open files in folders assigned to them or their groups, and files they own. Any other media returns 404, the same as a media id that does not exist.

#### Three kinds of comment

- **On a passage**: send `range` and `expectedTranscriptRevision` together. `expectedTranscriptRevision` is the `transcriptRevision` you read the transcript at, from `GET /media/transcript/{mediaId}`, `GET /media/insight/{mediaId}`, or the list endpoints on this page. The server builds the anchor from its own copy of the transcript. If the transcript has been saved since, the request returns 409 "This transcript changed since you opened it. Reload to keep editing." and nothing is written: read the transcript again and recompute the word range.
- **On the whole file**: leave out both `range` and `expectedTranscriptRevision`.
- **A reply**: send `parentId`, the `commentId` of a thread's first comment. A reply cannot carry `range` or `mediaLabelId`; it belongs to its thread. Replies go one level deep, so replying to a reply returns 400.

Add `mediaLabelId` to link a new thread to a labelled passage on the same media.

A reply sends an in-app notification to the person who started the thread, unless they wrote the reply. A transcript holds at most 5,000 comments on passages; whole-file comments and replies are not counted.

#### Errors

- `400` when a field fails validation, the rules above are broken, the range runs past the transcript, or the limit is reached.
- `403` without the permission.
- `404` when the media, the parent comment or the labelled passage does not exist.
- `409` when `expectedTranscriptRevision` is stale.

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

### 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 |
| --- | --- | --- |
| `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. |
| `mediaLabelId` | string | A labelled passage on this media to link the thread to. Not allowed on 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/media/b4994aa1267c/comments' \
  -H 'x-speakai-key: sk_test_speak_0000000000000000' \
  -H 'x-access-token: eyJhbGciOiJIUzI1NiJ9.test-access-token.0000000000' \
  -H 'Content-Type: application/json' \
  -d '{
  "body": "Can we confirm this date with finance?",
  "range": {
"start": 42,
"end": 49
  },
  "expectedTranscriptRevision": 3
}'
```

</CodePanel>

<ResponsePanel id="post-media-media-id-comments" statuses={[{"code":"200","description":"Success"},{"code":"400","description":"Validation, reply depth, range or limit problem"},{"code":"403","description":"You lack the Comments: Create permission"},{"code":"404","description":"The media, parent comment or labelled passage does not exist"},{"code":"409","description":"The transcript changed since you read it"}]}>

<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` -- 14 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. |
| `mediaLabelId` | string | Labelled passage the comment is linked to. Present only when one was linked. |
| `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. |
| `resolvedBy` | string | Id of the person who resolved the thread. Present only while it is resolved. |
| `isDeleted` | boolean | `true` on a deleted first comment kept because it has replies. |
| `userId` | string | Id of the author. |
| `createdAt` | string (date-time) | When it was posted. |
| `updatedAt` | string (date-time) | When it last changed. |

<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,
"userId": "65f0c1a2b3c4d5e6f7a8b9c0",
"createdAt": "2026-10-06T14:05:12.000Z",
"updatedAt": "2026-10-06T14:05:12.000Z"
  }
}
```

</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-403">

**`403` You lack the Comments: Create permission**

Response body, `application/json`.

| Field | Type | Description |
| --- | --- | --- |
| `status` | string | `failed`. One of: `failed` . |
| `message` | string | Why the request was refused. |

Example response (You lack the Comments: Create permission), `application/json`.

```json
{
  "status": "failed",
  "message": "You do not have permission to perform this action"
}
```

</div>

<div slot="status-404">

**`404` The media, parent comment or labelled passage does not exist**

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 media, parent comment or labelled passage does not exist), `application/json`.

```json
{
  "status": "failed",
  "requestId": "req_0000000000000000",
  "code": 404,
  "message": "Comment not found.",
  "hints": [
"The requested operation failed because a resource associated with the request could not be found."
  ]
}
```

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

</ResponsePanel>

</div>

<h2 id="patch-media-media-id-comments-comment-id">Edit, resolve or reopen a comment</h2>

Send exactly one of `body` or `isResolved`; both or neither returns 400.

- `body` edits the text. Only the author can edit a comment; anyone else gets 403 "You can only edit your own comments." No permission is needed to edit your own.
- `isResolved` resolves (`true`) or reopens (`false`) a thread. It needs the **Comments: Update** permission (on by default for admins and members) and works only on the first comment of a thread; a reply returns 400.

#### Errors

- `400` when the body breaks the rules above or fails validation.
- `403` when you edit someone else's comment (the error envelope shown), or resolve without the permission (`{"status": "failed", "message": "You do not have permission to perform this action"}`).
- `404` when the media or the comment does not exist, or the comment was deleted.

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

### 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 |
| --- | --- | --- |
| `body` | string | New text. Author only. Comment text. Trimmed, then 1 to 5,000 characters. |
| `isResolved` | boolean | `true` to resolve the thread, `false` to reopen it. First comment of a thread only. |

<div slot="code">

<CodePanel label="Example request">

```bash
curl -X PATCH 'https://api.speakai.co/v1/media/b4994aa1267c/comments/Cm8WpE3kLv6NdHa' \
  -H 'x-speakai-key: sk_test_speak_0000000000000000' \
  -H 'x-access-token: eyJhbGciOiJIUzI1NiJ9.test-access-token.0000000000' \
  -H 'Content-Type: application/json' \
  -d '{
  "isResolved": true
}'
```

</CodePanel>

<ResponsePanel id="patch-media-media-id-comments-comment-id" statuses={[{"code":"200","description":"Success"},{"code":"400","description":"The body broke a rule or failed validation"},{"code":"403","description":"Not your comment, or no Comments: Update permission"},{"code":"404","description":"The media or comment does not exist"}]}>

<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` -- 14 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. |
| `mediaLabelId` | string | Labelled passage the comment is linked to. Present only when one was linked. |
| `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. |
| `resolvedBy` | string | Id of the person who resolved the thread. Present only while it is resolved. |
| `isDeleted` | boolean | `true` on a deleted first comment kept because it has replies. |
| `userId` | string | Id of the author. |
| `createdAt` | string (date-time) | When it was posted. |
| `updatedAt` | string (date-time) | When it last changed. |

<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,
"userId": "65f0c1a2b3c4d5e6f7a8b9c0",
"createdAt": "2026-10-06T14:05:12.000Z",
"updatedAt": "2026-10-06T16:30:00.000Z",
"resolvedBy": "65f0c1a2b3c4d5e6f7a8b9c0"
  }
}
```

</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-403">

**`403` Not your comment, or no Comments: Update permission**

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 (Not your comment, or no Comments: Update permission), `application/json`.

```json
{
  "status": "failed",
  "requestId": "req_0000000000000000",
  "code": 403,
  "message": "You can only edit your own comments.",
  "hints": [
"The requested operation is forbidden and cannot be completed."
  ]
}
```

</div>

<div slot="status-404">

**`404` The media or comment does not exist**

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 media or comment does not exist), `application/json`.

```json
{
  "status": "failed",
  "requestId": "req_0000000000000000",
  "code": 404,
  "message": "Comment not found.",
  "hints": [
"The requested operation failed because a resource associated with the request could not be found."
  ]
}
```

</div>

</ResponsePanel>

</div>

<h2 id="delete-media-media-id-comments-comment-id">Delete a comment</h2>

You can always delete your own comments. Deleting someone else's needs the **Comments: Delete** permission (on by default for admins only); without it the request returns 403.

A deleted first comment that has replies stays in the list as an empty placeholder so the replies keep their context.

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

<div slot="code">

<CodePanel label="Example request">

```bash
curl -X DELETE 'https://api.speakai.co/v1/media/b4994aa1267c/comments/Cm8WpE3kLv6NdHa' \
  -H 'x-speakai-key: sk_test_speak_0000000000000000' \
  -H 'x-access-token: eyJhbGciOiJIUzI1NiJ9.test-access-token.0000000000'
```

</CodePanel>

<ResponsePanel id="delete-media-media-id-comments-comment-id" statuses={[{"code":"200","description":"Success"},{"code":"403","description":"Someone else's comment, and you lack Comments: Delete"},{"code":"404","description":"The media or comment does not exist, or it was already deleted"}]}>

<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-403">

**`403` Someone else's comment, and you lack Comments: Delete**

Response body, `application/json`.

| Field | Type | Description |
| --- | --- | --- |
| `status` | string | `failed`. One of: `failed` . |
| `message` | string | Why the request was refused. |

Example response (Someone else's comment, and you lack Comments: Delete), `application/json`.

```json
{
  "status": "failed",
  "message": "You do not have permission to perform this action"
}
```

</div>

<div slot="status-404">

**`404` The media or comment does not exist, or it was already deleted**

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 media or comment does not exist, or it was already deleted), `application/json`.

```json
{
  "status": "failed",
  "requestId": "req_0000000000000000",
  "code": 404,
  "message": "Comment not found.",
  "hints": [
"The requested operation failed because a resource associated with the request could not be found."
  ]
}
```

</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-comments).

Source: https://docs.speakai.co/api/comments/index.mdx
