---
title: "Upload audio and video to Speak AI and read insights"
description: "Upload audio and video to Speak AI, list your media library, poll processing status, read the transcript and insights, and run the analysis again."
---

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

# Upload audio and video to Speak AI and read insights


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

The Speak AI API exposes 10 media 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/).

This media collection of endpoints enables operation of list, insights, transcript and export.

## Uploading audio or video

When uploading a file, a media file URL (accessible to our servers) must be provided.

This can be a publicly accessible URL hosted by yourselves or a third-party. 

You can upload files directly to our storage (AWS S3 bucket) and upload the file using the returned URL.

### Step 2, PUT the file to the presigned URL

This step targets the storage host returned in `preSignedUrl`, not `api.speakai.co`, so it is described here rather than as a Speak AI endpoint.

### PUT Request to Upload Audio File

This endpoint allows you to upload an audio file (in this case, an MP3 file) to an Amazon S3 bucket. The request is made using the HTTP PUT method, which is commonly used for updating resources.

#### Request URL

```text
https://xs3.amazonaws.com/video/1622234682034-medianame.mp3?AWSAccessKeyId=x&Content-Type=audio/mp3&Expires=1622245482&Signature=nQWU4yxBuUjQJD1yTr92qblQx6s=&x-amz-acl=public-read

```

#### Query Parameters

- `AWSAccessKeyId`: Your AWS access key ID, used for authentication.

- `Content-Type`: Specifies the media type of the resource being uploaded (in this case, `audio/mp3`).

- `Expires`: A timestamp indicating when the URL will expire.

- `Signature`: A signature for authentication, ensuring the request is valid.

- `x-amz-acl`: Sets the access control list (ACL) for the uploaded file. Here, it is set to `public-read`, allowing public access to the file.

#### Request Body

The request does not specify a body type, indicating that the audio file is uploaded directly to the specified URL.

#### Expected Response

The response from a successful upload typically includes:

- A confirmation of the upload status (e.g., HTTP status code 200 OK).

- Metadata about the uploaded file, such as its URL and size.

Ensure that the audio file being uploaded adheres to the specified content type and that all required parameters are included in the request.

## What can you do with the media endpoints?

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

| Method | Path | What it does |
| --- | --- | --- |
| `GET` | [`/media`](#get-media) | List |
| `PUT` | [`/media/{mediaId}`](#put-media-media-id) | Update meta-data |
| `DELETE` | [`/media/{mediaId}`](#delete-media-media-id) | Delete File |
| `GET` | [`/media/insight/{mediaId}`](#get-media-insight-media-id) | Insights |
| `GET` | [`/media/reanalyze/{mediaId}`](#get-media-reanalyze-media-id) | Re-analyze a media file or text note |
| `PUT` | [`/media/speakers/{mediaId}`](#put-media-speakers-media-id) | Update Transcript Speakers |
| `GET` | [`/media/status/{mediaId}`](#get-media-status-media-id) | Status |
| `GET` | [`/media/transcript/{mediaId}`](#get-media-transcript-media-id) | Transcript |
| `POST` | [`/media/upload`](#post-media-upload) | Upload your file with the Signed Url or External URL |
| `GET` | [`/media/upload/signedurl`](#get-media-upload-signedurl) | Get a Signed Url |

<h2 id="get-media">List</h2>

If you are looking for a paginated, sorted and filtered listing of your media library.

- You can pass `userId` in `params` to get data for a user. (Only for enterprise use cases)

### Parameters

| Parameter | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `mediaType` | query | string | No | audio, video, text |
| `page` | query | integer | No | Default `1`st page |
| `pageSize` | query | integer | No | Default `50` |
| `sortBy` | query | string | No | Default Created At in Descending order Sort by: Created At, positive, neutral, negative `asc` or `desc` |
| `filterMedia` | query | integer | No | 0 -- Uploaded 1 -- Assigned Media to a User 2 -- Uploaded OR Assigned Default: `0` |
| `filterName` | query | string | No | Search from media name, keywords, labels, faces, brands, people, locations and transcription. |
| `folderId` | query | string | No | Pass folderId to fetch only those records |

<div slot="code">

<CodePanel label="Example request">

```bash
curl -X GET 'https://api.speakai.co/v1/media?mediaType=audio&page=1&pageSize=50&sortBy=createdAt%3Adesc&filterMedia=0&folderId=568sd56899' \
  -H 'x-speakai-key: sk_test_speak_0000000000000000' \
  -H 'x-access-token: eyJhbGciOiJIUzI1NiJ9.test-access-token.0000000000'
```

</CodePanel>

<ResponsePanel id="get-media" statuses={[{"code":"200","description":"OK"}]}>

<div slot="status-200">

**`200` OK**

Response body, `application/json`.

| Field | Type |
| --- | --- |
| `status` | string |
| `data` | object |
| `data.totalCount` | integer |
| `data.pages` | integer |
| `data.mediaList` | object[] |

Deeper nested fields are not listed. See the example response below for the full shape.

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

```json
{
  "status": "success",
  "data": {
"totalCount": 71,
"pages": 71,
"mediaList": [
  {
    "duration": {
      "inSecond": "60",
      "start": "0:00:00",
      "end": "0:01:00.182"
    },
    "privacyMode": "Public",
    "processingProgress": "100",
    "state": "processed",
    "remark": "",
    "_id": "5f46c87668ee9913005e5bef",
    "sentiment": [],
    "mediaId": "50ek03r4kebud105",
    "name": "vatsal-30",
    "description": "url-test",
    "createdAt": "2020-08-26T20:39:18.870Z"
  }
]
  }
}
```

</div>

</ResponsePanel>

</div>

<h2 id="put-media-media-id">Update meta-data</h2>

If you want to rename, edit description or add a tag to your media, you can do it here.

### Parameters

| Parameter | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `mediaId` | path | string | Yes |  |

### 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 |
| --- | --- | --- |
| `createdAt` | string | The creation date to store on the file. Optional. An empty string is allowed, but the controller only writes the value when it is truthy, so an empty string does not clear the existing date. |
| `description` | string | A short description for the file. Optional. Trimmed. An empty string is allowed by validation, but the controller only writes a non empty value, so you cannot clear the description this way. |
| `folderId` | string | The id of the folder you want to move the file into. Optional. An empty string is allowed but is treated as no change. |
| `mediaId` | string | Ignored. The server takes the media id from the path, not from the body. Optional. An empty string is allowed. Accepted by validation and never destructured by the controller. |
| `manageBy` | string | The id of the user you want the file assigned to. Optional. An empty string is allowed but is treated as no change. Stored as a reference to a user record (media model: Schema.Types.ObjectId, ref 'User'). |
| `name` | string, **required** | The new title for the file. Required. Minimum length 1. This is the only property the controller always writes, so every update overwrites the title. |
| `remark` | string | A note stored with the file. Optional. An empty string is allowed but is treated as no change. |
| `status` | string | The workflow status of the file. Optional and unconstrained by the schema, so any string passes validation. The media model recognizes '', pending, progress, completed and pendingPayment, and the update runs through findOneAndUpdate without runValidators, so the model enum is not enforced on this path. Only written when not empty. |
| `tags` | string[] | The full list of tags for the file. The list you send replaces the existing one. Optional. Items are strings and empty strings are allowed. There is no cap on the number of items. Only written when the property is present, and an empty array is truthy so it does clear the tags. |

<div slot="code">

<CodePanel label="Example request">

```bash
curl -X PUT 'https://api.speakai.co/v1/media/MEDIA_ID' \
  -H 'x-speakai-key: sk_test_speak_0000000000000000' \
  -H 'x-access-token: eyJhbGciOiJIUzI1NiJ9.test-access-token.0000000000' \
  -H 'Content-Type: application/json' \
  -d '{
  "name": "update-1",
  "description": "description-1",
  "tags": [
"work",
"personal"
  ]
}'
```

</CodePanel>

<ResponsePanel id="put-media-media-id" statuses={[{"code":"200","description":"OK"},{"code":"404","description":"Not Found"}]}>

<div slot="status-200">

**`200` OK**

Response body, `application/json`.

| Field | Type |
| --- | --- |
| `status` | string |
| `message` | string |

Example response (Update Media data - success), `application/json`.

```json
{
  "status": "success",
  "message": "Updated successfully."
}
```

</div>

<div slot="status-404">

**`404` Not Found**

Response body, `application/json`.

| Field | Type |
| --- | --- |
| `status` | string |
| `requestId` | string |
| `code` | integer |
| `message` | string |
| `hints` | string[] |

Example response (Update Media data - failure), `application/json`.

```json
{
  "status": "failed",
  "requestId": "7ed88c1e-e1ee-44b0-afa5-50041d2b2309",
  "code": 404,
  "message": "Media does not exist!",
  "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">Delete File</h2>

If you want to remove media from your library - you can do it here.

We can revert the changes if you send us a request.

### Parameters

| Parameter | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `mediaId` | path | string | Yes |  |

<div slot="code">

<CodePanel label="Example request">

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

</CodePanel>

<ResponsePanel id="delete-media-media-id" statuses={[{"code":"200","description":"Success"}]}>

<div slot="status-200">

**`200` Success**

The spec records this status code with no example body.

</div>

</ResponsePanel>

</div>

<h2 id="get-media-insight-media-id">Insights</h2>

Load the results of analysis (sentiment, transcription, named entities, etc) for your media object here.

- You can pass `userId` in `params` to get specifically for any user.

### Parameters

| Parameter | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `mediaId` | path | string | Yes |  |

<div slot="code">

<CodePanel label="Example request">

```bash
curl -X GET 'https://api.speakai.co/v1/media/insight/5a4690abf255' \
  -H 'x-speakai-key: sk_test_speak_0000000000000000' \
  -H 'x-access-token: eyJhbGciOiJIUzI1NiJ9.test-access-token.0000000000'
```

</CodePanel>

<ResponsePanel id="get-media-insight-media-id" statuses={[{"code":"200","description":"OK"}]}>

<div slot="status-200">

**`200` OK**

Response body, `application/json`.

| Field | Type |
| --- | --- |
| `status` | string |
| `data` | object |
| `data.insight` | object |
| `data.duration` | object |
| `data.summary` | object |
| `data.tags` | any[] |
| `data.isTranscriptionOnly` | boolean |
| `data.isVideo` | boolean |
| `data.processingProgress` | string |
| `data.state` | string |
| `data.remark` | string |
| `data.sentiment` | object[] |
| `data.createdAt` | string (date-time) |
| `data.updatedAt` | string (date-time) |
| `data.name` | string |
| `data.description` | string |
| `data.publishedUrl` | string |
| `data.mediaId` | string |
| `data.originalCreatedAt` | string (date-time) |
| `data.faceSpriteImg` | string |

Deeper nested fields are not listed. See the example response below for the full shape.

Example response (success), `application/json`. Arrays are shortened to one entry and long strings are cut.

```json
{
  "status": "success",
  "data": {
"insight": {
  "isAnalyze": true,
  "transcript": [
    {
      "id": 1,
      "text": "Both. Hello hello good how are you?",
      "confidence": 0.8278,
      "speakerId": 1,
      "language": "en-US",
      "instances": [
        {
          "adjustedStart": "0:00:00",
          "adjustedEnd": "0:00:05.92",
          "start": "0:00:00",
          "end": "0:00:05.92"
        }
      ]
    }
  ],
  "faces": [],
  "keywords": [
    {
      "isDeleted": false,
      "instances": [
        {
          "adjustedStart": "0:00:08.44",
          "adjustedEnd": "0:00:13.84",
          "start": "0:00:08.44",
          "end": "0:00:13.84"
        }
      ],
      "id": 1,
      "name": "company"
    }
  ],
  "labels": [],
  "brands": [],
  "topics": [
    {
      "isDeleted": false,
      "instances": [
        {
          "adjustedStart": "0:00:00",
          "adjustedEnd": "0:00:47.744",
          "start": "0:00:00",
          "end": "0:00:47.744"
        }
      ],
      "id": 1,
      "name": "Promotions"
    }
  ],
  "people": [],
  "locations": [],
  "arts": [],
  "dates": [],
  "events": [],
  "geopolitical": [],
  "languages": [],
  "laws": [],
  "money": [],
  "ordinals": [],
  "percentages": [],
  "quantities": [],
  "times": [],
  "cardinals": [],
  "nationalities": [],
  "products": []
},
"duration": {
  "inSecond": "47",
  "end": "0:00:47.744",
  "start": "0:00:00"
},
"summary": {
  "original": {
    "text": "",
    "createdAt": "2020-09-04T16:16:19.118Z"
  }
},
"tags": [],
"isTranscriptionOnly": false,
"isVideo": false,
"processingProgress": "100",
"state": "processed",
"remark": "",
"sentiment": [
  {
    "document": {
      "Negative": 0,
      "Neutral": 0,
      "Positive": 0
    },
    "sentences": [
      {
        "id": 1,
        "instances": [],
        "score": {
          "compound": 0,
          "neg": 0,
          "neu": 1,
          "pos": 0
        },
        "text": "Sentence 1"
      }
    ]
  }
],
"createdAt": "2020-08-22T23:05:57.000Z",
"updatedAt": "2020-08-22T23:11:11.288Z",
"name": "REc557e3c8f2ac42a828973c39f1d6be37",
"description": "",
"publishedUrl": "media/5d03a9d5d4bca272e9c8cf89/audio/1598137553998-rec557e3c8f2ac42a828973c39f1d6be37.mp3",
"mediaId": "81c226f66705",
"originalCreatedAt": "2020-11-08T16:40:10.706Z",
"faceSpriteImg": ""
  }
}
```

Example response (Insights - notUploaded), `application/json`.

```json
{
  "status": 200,
  "state": "notUploaded",
  "message": {
"insight": {
  "isAnalyze": false,
  "faces": [],
  "keywords": [],
  "sentiments": [],
  "emotions": [],
  "labels": [],
  "brands": [],
  "topics": [],
  "transcript": [],
  "speakers": []
},
"duration": {
  "inSecond": "0"
},
"createdAt": "2019-09-25T14:32:19.000Z",
"_id": "5d8b7a730838363d90a17e86",
"videoId": "QHw2hPhk9-R7rrlPKBZk-FBY9P8rDBV",
"name": "ForBiggerBlazes - Copy",
"description": "",
"privacyMode": "Public",
"size": "0",
"processingProgress": "0",
"state": "notUploaded",
"publishedUrl": "video/5cc37a170eb2173efcfc1f7d/1569421938203-ForBiggerBlazesCopymp4.mp4",
"videoFormat": 1,
"sourceLanguage": "en-US",
"order": 0,
"__v": 0
  }
}
```

</div>

</ResponsePanel>

</div>

<h2 id="get-media-reanalyze-media-id">Re-analyze a media file or text note</h2>

Run the Speak AI analysis again over a record that has already been processed, and replace the insights it produced the first time.

One route covers both kinds of record. Pass the `mediaId` of an uploaded audio or video file, or the `mediaId` of a text note, and Speak AI re-analyzes whichever record the id belongs to.

#### What the call does

The record moves into the `preparingAnalysis` state and the analysis runs in the background, so the response comes back before the new insights exist. Poll `GET /media/status/{mediaId}` to follow it, or subscribe to the `media.reanalyzed` and `text.reanalyzed` webhook events.

The query parameters select which parts of the analysis run again. Omit all of them to re-run the default analysis.

### Parameters

| Parameter | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `mediaId` | path | string | Yes | Id of the media file or text note to re-analyze. |
| `isInsights` | query | boolean | No | Re-run insight extraction. |
| `isSentiment` | query | boolean | No | Re-run sentiment analysis. |
| `isFillerWords` | query | boolean | No | Re-run filler word detection. |
| `isEmbeddings` | query | boolean | No | Rebuild the embeddings used by AI chat. |

<div slot="code">

<CodePanel label="Example request">

```bash
curl -X GET 'https://api.speakai.co/v1/media/reanalyze/3f8d1ec6ca39?isInsights=true&isSentiment=true&isFillerWords=true&isEmbeddings=true' \
  -H 'x-speakai-key: sk_test_speak_0000000000000000' \
  -H 'x-access-token: eyJhbGciOiJIUzI1NiJ9.test-access-token.0000000000'
```

</CodePanel>

<ResponsePanel id="get-media-reanalyze-media-id" statuses={[{"code":"200","description":"OK"},{"code":"500","description":"Internal Server Error"}]}>

<div slot="status-200">

**`200` OK**

Response body, `application/json`.

| Field | Type |
| --- | --- |
| `status` | string |
| `message` | string |

Example response (Re-analysis started), `application/json`.

```json
{
  "status": "success",
  "message": "Leon Wednesday Meeting re-analyzed successfully!"
}
```

Example response (Record already in the failed state), `application/json`.

```json
{
  "status": "success",
  "message": "Media analysis failed. Please contact support if you need assistance."
}
```

</div>

<div slot="status-500">

**`500` Internal Server Error**

Response body, `application/json`.

| Field | Type |
| --- | --- |
| `status` | string |
| `requestId` | string |
| `code` | integer |
| `message` | string |
| `hints` | string[] |

Example response (Internal server error), `application/json`.

```json
{
  "status": "failed",
  "requestId": "ca665625-645e-438a-867d-366a6e3f133a",
  "code": 500,
  "message": "Something went wrong. Please try again later.",
  "hints": [
"Something went wrong. Please try again later."
  ]
}
```

</div>

</ResponsePanel>

</div>

<h2 id="put-media-speakers-media-id">Update Transcript Speakers</h2>

#### Request Description

This API endpoint allows you to update speaker names in the transcription of a media object.

##### Endpoint

`PUT /v1/media/speakers/{mediaId}`

##### Method

`PUT`

##### Purpose

The purpose of this request is to update the speaker information associated with a specific media object. This includes modifying the names of the speakers in the transcription data.

##### Input Parameters

- `mediaId` (path parameter): The unique identifier for the media object whose speaker information you wish to update.

- **Request Body**: The request body should be a JSON array containing objects with the following structure:

- **id** (string): The unique identifier for the speaker **(ORIGINAL)**.

- **name** (string): The new name of the speaker.

##### Response Structure

The response will return a JSON object containing the following fields:

- **status**: Indicates the status of the request.

- **message**: String

##### Example Response

``` json
{
 "status": "",
 "message": ""
}

```

##### Notes

- Ensure that the `mediaId` provided in the URL corresponds to an existing media object.

- The request body must be formatted as a JSON array, even if updating a single speaker.

- The response will confirm the status of the update operation, along with any relevant data about the media analysis.

**Request rules.** The body is an array, one entry per speaker you are relabelling, and every entry needs `id` and `name`. Omit `segmentIds` to relabel every segment for that speaker, or pass them to limit the rename to one paragraph. Pass `mergeIntoId` to merge this speaker into an existing one.

### Parameters

| Parameter | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `mediaId` | path | string | Yes |  |

### 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 |
| --- | --- | --- |
| `[].id` | string, **required** | The speaker you are renaming, given as either the speaker's current label or its numeric speaker id. Required on every array entry. Trimmed. The server matches the current label first and the numeric id second (resolveTargetSpeaker at, so a label wins when both could match. |
| `[].name` | string, **required** | The new name for that speaker. Required on every array entry. Trimmed. Reusing another speaker's name returns 400 unless you also send mergeIntoId on the same entry. |
| `[].segmentIds` | number[] | The transcript segment ids you want renamed. Send it to limit the change to one paragraph, and leave it out to rename every segment belonging to that speaker. Optional. Items must be numbers. No minimum or maximum item count. |
| `[].mergeIntoId` | string | The id of the speaker you are merging this one into. Sending it confirms you meant to combine two speakers, so the duplicate name check is skipped for this entry. Optional. An empty string is allowed, and an empty string counts as not set. |

<div slot="code">

<CodePanel label="Example request">

```bash
curl -X PUT 'https://api.speakai.co/v1/media/speakers/b4994aa1267c' \
  -H 'x-speakai-key: sk_test_speak_0000000000000000' \
  -H 'x-access-token: eyJhbGciOiJIUzI1NiJ9.test-access-token.0000000000' \
  -H 'Content-Type: application/json' \
  -d '[
  {
"id": "0",
"name": "Vatsal"
  }
]'
```

</CodePanel>

<ResponsePanel id="put-media-speakers-media-id" statuses={[{"code":"200","description":"OK"}]}>

<div slot="status-200">

**`200` OK**

Response body, `application/json`.

| Field | Type |
| --- | --- |
| `status` | string |
| `data` | object |
| `data.insight` | object |
| `data.duration` | object |
| `data.summary` | object |
| `data.tags` | any[] |
| `data.isTranscriptionOnly` | boolean |
| `data.isVideo` | boolean |
| `data.processingProgress` | string |
| `data.state` | string |
| `data.remark` | string |
| `data.sentiment` | object[] |
| `data.createdAt` | string (date-time) |
| `data.updatedAt` | string (date-time) |
| `data.name` | string |
| `data.description` | string |
| `data.publishedUrl` | string |
| `data.mediaId` | string |
| `data.originalCreatedAt` | string (date-time) |
| `data.faceSpriteImg` | string |

Deeper nested fields are not listed. See the example response below for the full shape.

Example response (success), `application/json`. Arrays are shortened to one entry and long strings are cut.

```json
{
  "status": "success",
  "data": {
"insight": {
  "isAnalyze": true,
  "transcript": [
    {
      "id": 1,
      "text": "Both. Hello hello good how are you?",
      "confidence": 0.8278,
      "speakerId": 1,
      "language": "en-US",
      "instances": [
        {
          "adjustedStart": "0:00:00",
          "adjustedEnd": "0:00:05.92",
          "start": "0:00:00",
          "end": "0:00:05.92"
        }
      ]
    }
  ],
  "faces": [],
  "keywords": [
    {
      "isDeleted": false,
      "instances": [
        {
          "adjustedStart": "0:00:08.44",
          "adjustedEnd": "0:00:13.84",
          "start": "0:00:08.44",
          "end": "0:00:13.84"
        }
      ],
      "id": 1,
      "name": "company"
    }
  ],
  "labels": [],
  "brands": [],
  "topics": [
    {
      "isDeleted": false,
      "instances": [
        {
          "adjustedStart": "0:00:00",
          "adjustedEnd": "0:00:47.744",
          "start": "0:00:00",
          "end": "0:00:47.744"
        }
      ],
      "id": 1,
      "name": "Promotions"
    }
  ],
  "people": [],
  "locations": [],
  "arts": [],
  "dates": [],
  "events": [],
  "geopolitical": [],
  "languages": [],
  "laws": [],
  "money": [],
  "ordinals": [],
  "percentages": [],
  "quantities": [],
  "times": [],
  "cardinals": [],
  "nationalities": [],
  "products": []
},
"duration": {
  "inSecond": "47",
  "end": "0:00:47.744",
  "start": "0:00:00"
},
"summary": {
  "original": {
    "text": "",
    "createdAt": "2020-09-04T16:16:19.118Z"
  }
},
"tags": [],
"isTranscriptionOnly": false,
"isVideo": false,
"processingProgress": "100",
"state": "processed",
"remark": "",
"sentiment": [
  {
    "document": {
      "Negative": 0,
      "Neutral": 0,
      "Positive": 0
    },
    "sentences": [
      {
        "id": 1,
        "instances": [],
        "score": {
          "compound": 0,
          "neg": 0,
          "neu": 1,
          "pos": 0
        },
        "text": "Sentence 1"
      }
    ]
  }
],
"createdAt": "2020-08-22T23:05:57.000Z",
"updatedAt": "2020-08-22T23:11:11.288Z",
"name": "REc557e3c8f2ac42a828973c39f1d6be37",
"description": "",
"publishedUrl": "media/5d03a9d5d4bca272e9c8cf89/audio/1598137553998-rec557e3c8f2ac42a828973c39f1d6be37.mp3",
"mediaId": "81c226f66705",
"originalCreatedAt": "2020-11-08T16:40:10.706Z",
"faceSpriteImg": ""
  }
}
```

</div>

</ResponsePanel>

</div>

<h2 id="get-media-status-media-id">Status</h2>

If you need a list or a quick preview of a media you can request meta-data about media objects with this endpoint, this will include all information except for results of analysis, transcripts, etc.

### Parameters

| Parameter | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `mediaId` | path | string | Yes |  |

<div slot="code">

<CodePanel label="Example request">

```bash
curl -X GET 'https://api.speakai.co/v1/media/status/MEDIA_ID' \
  -H 'x-speakai-key: sk_test_speak_0000000000000000' \
  -H 'x-access-token: eyJhbGciOiJIUzI1NiJ9.test-access-token.0000000000'
```

</CodePanel>

<ResponsePanel id="get-media-status-media-id" statuses={[{"code":"200","description":"OK"}]}>

<div slot="status-200">

**`200` OK**

Response body, `application/json`.

| Field | Type |
| --- | --- |
| `status` | string |
| `data` | object |
| `data.duration` | object |
| `data.tags` | any[] |
| `data.isVideo` | boolean |
| `data.processingProgress` | string |
| `data.state` | string |
| `data.remark` | string |
| `data._id` | string |
| `data.createdAt` | string (date-time) |
| `data.updatedAt` | string (date-time) |
| `data.name` | string |
| `data.description` | string |
| `data.publishedUrl` | string |
| `data.mediaId` | string |
| `data.originalCreatedAt` | string (date-time) |

Deeper nested fields are not listed. See the example response below for the full shape.

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

```json
{
  "status": "success",
  "data": {
"duration": {
  "inSecond": "47",
  "end": "0:00:47.744",
  "start": "0:00:00"
},
"tags": [],
"isVideo": false,
"processingProgress": "100",
"state": "processed",
"remark": "",
"_id": "5f41a4d5c35d8d0fd556e075",
"createdAt": "2020-08-22T23:05:57.000Z",
"updatedAt": "2020-08-22T23:11:11.288Z",
"name": "REc557e3c8f2ac42a828973c39f1d6be37",
"description": "",
"publishedUrl": "media/5d03a9d5d4bca272e9c8cf89/audio/1598137553998-rec557e3c8f2ac42a828973c39f1d6be37.mp3",
"mediaId": "81c226f66705",
"originalCreatedAt": "2020-11-08T16:40:10.706Z"
  }
}
```

Example response (Insights - notUploaded), `application/json`.

```json
{
  "status": 200,
  "state": "notUploaded",
  "message": {
"insight": {
  "isAnalyze": false,
  "faces": [],
  "keywords": [],
  "sentiments": [],
  "emotions": [],
  "labels": [],
  "brands": [],
  "topics": [],
  "transcript": [],
  "speakers": []
},
"duration": {
  "inSecond": "0"
},
"createdAt": "2019-09-25T14:32:19.000Z",
"_id": "5d8b7a730838363d90a17e86",
"videoId": "QHw2hPhk9-R7rrlPKBZk-FBY9P8rDBV",
"name": "ForBiggerBlazes - Copy",
"description": "",
"privacyMode": "Public",
"size": "0",
"processingProgress": "0",
"state": "notUploaded",
"publishedUrl": "video/5cc37a170eb2173efcfc1f7d/1569421938203-ForBiggerBlazesCopymp4.mp4",
"videoFormat": 1,
"sourceLanguage": "en-US",
"order": 0,
"__v": 0
  }
}
```

</div>

</ResponsePanel>

</div>

<h2 id="get-media-transcript-media-id">Transcript</h2>

### Parameters

| Parameter | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `mediaId` | path | string | Yes |  |

<div slot="code">

<CodePanel label="Example request">

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

</CodePanel>

<ResponsePanel id="get-media-transcript-media-id" statuses={[{"code":"200","description":"OK"}]}>

<div slot="status-200">

**`200` OK**

Response body, `application/json`.

| Field | Type |
| --- | --- |
| `status` | string |
| `data` | object |
| `data.folderId` | string |
| `data.mediaType` | string |
| `data.name` | string |
| `data.sourceLanguage` | string |
| `data.insight` | object |
| `data.duration` | object |
| `data.createdAt` | string (date-time) |
| `data.mediaId` | string |
| `data.mediaUrl` | string |

Deeper nested fields are not listed. See the example response below for the full shape.

Example response (Transcript), `application/json`. Arrays are shortened to one entry and long strings are cut.

```json
{
  "status": "success",
  "data": {
"folderId": "0648e533f72c",
"mediaType": "audio",
"name": "vatsal",
"sourceLanguage": "en-US",
"insight": {
  "transcript": [
    {
      "id": 0,
      "text": "Okay. Let's do the testing for transcription.",
      "speakerId": "0",
      "confidence": 1,
      "language": "en-US",
      "instances": [
        {
          "start": "00:00:00.240",
          "end": "00:00:02.994",
          "startInSec": 0.24,
          "endInSec": 2.9940499999999997
        }
      ],
      "entities": [
        {
          "id": "1",
          "text": "Okay.",
          "confidence": 0.99782515,
          "speakerId": "0",
          "language": "en-US",
          "instances": {
            "startInSec": 0.24,
            "endInSec": 0.48
          }
        }
      ]
    }
  ],
  "speakers": [
    {
      "id": 0,
      "name": "0",
      "instances": [
        {
          "start": "00:00:00.240",
          "end": "00:00:02.994",
          "startInSec": 0.24,
          "endInSec": 2.9940499999999997
        }
      ],
      "totalWords": 10,
      "duration": 3,
      "wpm": 200
    }
  ]
},
"duration": {
  "start": "00:00:00.240",
  "end": "00:00:12.144",
  "inSecond": 12.144
},
"createdAt": "2025-10-17T17:25:57.608Z",
"mediaId": "b4994aa1267c",
"mediaUrl": "MEDIA_URL"
  }
}
```

</div>

</ResponsePanel>

</div>

<h2 id="post-media-upload">Upload your file with the Signed Url or External URL</h2>

If you want to add a hosted media (you must have a valid `URL`) to your media library, do it here.

#### Parameters

Please must specify `mediaType` as `audio` or `video`

```text
name: String required
description: string;
url: string required; // public URL or AWS signed URL
mediaType: 'audio'; // 'video'
userId: ""; // for enterprise users only (optional)
medicalId: "" // custom media Id or medical Id for your reference (optional)
createdAt: "2021-08-09T00:13:11.717Z" // must match the date format (optional)
tags: "comma separated strings",
folderId: "folder Id" // to save in the folder
fields: { id: string, value: string } = []; (Optional)

```

#### Webhook Callback URL:

- If you created a webhook for media events and want to receive a callback to a specific URL for this text note, you can assign `callbackUrl` - `string` in a `req.body`.

- Speak will consider the `callbackUrl` and will skip the callbackUrl created via Webhook API.

**Fields:**

You can pass the **id and value** in the Fields array to include related to your media files.

**Upload Considerations:**

- Maximum duration limit of 3 hours for a media URL.

- The URL needs to be accessible publicly. For Example - Google Drive, Dropbox are not supported.

- Valid YouTube URL examples are: `https://www.youtube.com/watch?v=pTJ-yvNWCUE`.

- **Supported File Formats are**:

 - Audio - `mp3 (recommended), m4a, wav, ogg, webm, m4p`

 - Video - `mp4 (recommended), m4v, wmv, avi, mov, flv`

### 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 |
| --- | --- | --- |
| `name` | string, **required** | The title you want the uploaded file to have in Speak. Required. Trimmed. No maximum length and no pattern. |
| `description` | string | A short description stored alongside the file. Optional. Trimmed. An empty string is allowed. The schema declares a default of an empty string, but the validation middleware discards the schema defaults, so an omitted value simply stays unset. |
| `url` | string, **required** | The link Speak fetches the file from. YouTube, Vimeo, Google Drive, Amazon S3 and the social platforms the resolver supports are all handled. Required. Plain string with no uri format check, so the server does not reject a malformed link at validation time. |
| `isVideo` | boolean | Set this to true when the file is video. It is stored on the media record, and the server also uses it to choose the media type when you do not send mediaType. An explicit false is what opts a YouTube link out of video. Optional. No default is applied to the request body. The stored value is youtubeIsAudioOnly ? false: youtubeResolvedVideo \|\| isVideo \|\| false, so when the YouTube resolver runs the stored value follows what the resolver returned rather than what you sent. |
| `mediaType` | string | The kind of file you are uploading. Optional. Lowercased before validation, so uppercase values such as VIDEO are accepted. The shared validator is. Values come from MediaType:1-7 (audio, video, text, media, csv) plus MediaFilterType (all). 'all' is a filter value that leaked into this schema, so it validates but is not a real upload type. For any file the server probes directly it overwrites your value with the type it detects (mediaType = FormatDetails.codecType.toLowerCase), and the YouTube resolver overwrites it too. One of: `audio`, `video`, `text`, `media`, `csv`, `all`. |
| `sourceLanguage` | string | The language spoken in the file, given as a transcription language code such as en-US. Optional. Any string passes, because the validator substitutes rather than rejects. An unrecognized code becomes auto. The schema declares a default of 'en-US', but that default is discarded by the middleware, so if you omit it the server uses the source language on your account onboarding and falls back to auto. Defined. |
| `userId` | string | Not used. The upload is always recorded against the authenticated user, so sending this changes nothing. Optional. Accepted by validation and never read by the controller, which takes userId off the request rather than the body. |
| `callbackUrl` | string | A URL you want Speak to call when the file finishes processing. Optional. Trimmed. An empty string is allowed. Stored on the media as webhookMetaData.callbackUrl. No uri format check. |
| `tags` | string | Tags to attach to the file, as a single comma separated string. Optional. Declared as a union, the schema) and allows null and an empty string, so an array also passes validation. The controller only reads a string and splits it on commas, so an array is accepted and then ignored. |
| `createdAt` | string | The creation timestamp to store on the file instead of the time of upload. Optional. An empty string is allowed. Parsed with new Date, so send an ISO 8601 string. |
| `isTranscriptionOnly` | boolean | Not used by this endpoint. Nothing in the upload flow reads it. Optional. Declared with a default of false, but that default is discarded and the value is never destructured or read by the controller. |
| `uploadType` | string | Where the upload came from. Send vimeo or zoom to pair the file with uploadReferenceId. Anything else is stored as you send it, and an omitted value is stored as api. Optional. Trimmed. No enum, so any string is accepted. The schema declares a default of 'api' that is discarded, and the controller re-applies it with uploadType \|\| 'api'. |
| `folderId` | string | The folder you want the file to land in. If no folder with that id exists on your account, the server puts the file in your first folder. Optional. An empty string is allowed. |
| `uploadReferenceId` | string | The id of the source recording when you upload from Zoom or Vimeo. It is stored as integrations.zoomId when uploadType is zoom and integrations.vimeoId when uploadType is vimeo. Optional, unconstrained and allows an empty string. OpenAPI has no any type, so the spec should omit the type keyword rather than pick one. |
| `vimeoDurationInSec` | number | The length of the video in seconds when you upload from Vimeo. The server uses it as the duration instead of probing the file. Optional. No minimum and no maximum. Only read when uploadType is vimeo. |
| `fields` | object[] | Custom field values to attach to the file. Each entry pairs the id of a field you created in Speak with the value you want stored. Optional. An unconstrained array, so validation does not check the item shape and does not cap the length. The controller expects objects with id and value, checks each id against your account fields, and silently drops the ones that do not match (checkForValidFields ends with.filter(Boolean)). |

<div slot="code">

<CodePanel label="Example request">

```bash
curl -X POST 'https://api.speakai.co/v1/media/upload' \
  -H 'x-speakai-key: sk_test_speak_0000000000000000' \
  -H 'x-access-token: eyJhbGciOiJIUzI1NiJ9.test-access-token.0000000000' \
  -H 'Content-Type: application/json' \
  -d '{
  "name": "Vatsal Test 1",
  "description": "Vatsal Description",
  "url": "https://kns.im/msgs/files/32dc84df-9650-4c17-b633-e80a5097ba6f.ogg",
  "mediaType": "video",
  "sourceLanguage": "he-IL",
  "tags": "your custom tags1, tag 2, tag 3",
  "fields": [
{
  "id": "FIELD_ID_CREATED_BY_SPEAK",
  "value": "YOUR_VALUE"
}
  ]
}'
```

</CodePanel>

<ResponsePanel id="post-media-upload" statuses={[{"code":"200","description":"OK"}]}>

<div slot="status-200">

**`200` OK**

Response body, `application/json`.

| Field | Type |
| --- | --- |
| `status` | string |
| `data` | object |
| `data.mediaId` | string |
| `data.state` | string |

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

```json
{
  "status": "success",
  "data": {
"mediaId": "86a36b59b38e",
"state": "uploaded"
  }
}
```

</div>

</ResponsePanel>

</div>

<h2 id="get-media-upload-signedurl">Get a Signed Url</h2>

If you have a locally saved file and you want to upload it to our system using API, you'll have to follow through 3 simple steps:

#### Process

1. Run this endpoint to retrieve an upload link for a placeholder on our storage bucket. You'll have `preSignedUrl.`

2. This `preSignedUrl` is your ticket to which you `PUT` your local file using `curl` or any other HTTP client.

3. Use "Upload your file with the Signed Url or External URL" to upload your file on Speak.

#### How does this work?

You can find more information about uploading a file using signed URLs [here](https://docs.aws.amazon.com/AmazonS3/latest/userguide/PresignedUrlUploadObject.html).
If you still find this unenlightening, shoot a message to us on success@speakai.co!

#### Expiry

The link will expire in 2 hours.

### Parameters

| Parameter | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `isVideo` | query | boolean | Yes | TRUE or FALSE (To Indentify is it `audio` or `video`. |
| `filename` | query | string | Yes | Filename with the extension is required |
| `mimeType` | query | string | No | audio/mp3 or audio/wav or video/mp4 |

<div slot="code">

<CodePanel label="Example request">

```bash
curl -X GET 'https://api.speakai.co/v1/media/upload/signedurl?isVideo=false&filename=media_name.mp3&mimeType=audio%2Fmp3' \
  -H 'x-speakai-key: sk_test_speak_0000000000000000' \
  -H 'x-access-token: eyJhbGciOiJIUzI1NiJ9.test-access-token.0000000000'
```

</CodePanel>

<ResponsePanel id="get-media-upload-signedurl" statuses={[{"code":"200","description":"Success"}]}>

<div slot="status-200">

**`200` Success**

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/)
- [Create and update live transcription sessions in Speak AI](/api/live-transcription/)
- [Analyze text notes with the Speak AI text endpoints](/api/text/)
- [Export Speak AI transcripts and insights to a file](/api/exports/)

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

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