> For the complete documentation index, see [llms.txt](https://docs.firework.com/firework-for-developers/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.firework.com/firework-for-developers/api/videos.md).

# Videos

### 1. Overview

The Firework Video API allows you to upload videos to the Firework platform programmatically. This API supports video file uploads with rich metadata including product associations.

**Base URL**: `https://api.firework.com`

### 2. Authentication

The Firework Video API uses OAuth 2.0 for authentication. Before using this API, you must obtain an access token.

**Authentication Methods Supported:**

* **Client Credentials**: OAuth 2.0 Client Credentials flow for server-to-server authentication (machine-to-machine)

> 📖 **Documentation:**
>
> * [Client Credentials OAuth](/firework-for-developers/api/authentication.md) - Server-to-server authentication for OAuth apps

***

### 3. Endpoint Summary

| Endpoint                                             | Scope          | Notes                                           |
| ---------------------------------------------------- | -------------- | ----------------------------------------------- |
| `POST /api/v1/upload_signatures`                     | `videos:write` | Get pre-signed credentials for S3 upload        |
| `POST /api/v1/upload_multipart/signatures`           | `videos:write` | Initiate multipart upload and get signed parts  |
| `POST /api/v1/upload_multipart/complete`             | `videos:write` | Complete a multipart upload with ETags          |
| `POST /api/v1/videos`                                | `videos:write` | Video creation with file upload                 |
| `POST /api/v1/videos`                                | `videos:write` | Video creation from URL (sync, default)         |
| `POST /api/v1/videos`                                | `videos:write` | Video creation from URL (async: `"async":true`) |
| `POST /api/v1/videos`                                | `videos:write` | Video creation from S3 key (application/json)   |
| `POST /api/v1/videos`                                | `videos:write` | Video creation from inline base64 (≤5MB clips)  |
| `GET /api/v1/videos`                                 | `videos:read`  | List videos in a channel (cursor-paginated)     |
| `GET /api/v1/videos/{id}`                            | `videos:read`  | Get video by ID                                 |
| `PATCH /api/v1/videos/{id}`                          | `videos:write` | Video updates                                   |
| `DELETE /api/v1/videos/{id}`                         | `videos:write` | Delete a video (204 No Content)                 |
| `POST /api/v1/videos/{id}/archive`                   | `videos:write` | Archive a video                                 |
| `POST /api/v1/videos/{id}/unarchive`                 | `videos:write` | Unarchive a video                               |
| `POST /api/v1/videos/{id}/publish`                   | `videos:write` | Publish now or schedule (`published_at`)        |
| `POST /api/v1/videos/{id}/unpublish`                 | `videos:write` | Unpublish (revert to draft)                     |
| `POST /api/v1/videos/{id}/subtitles`                 | `videos:write` | Add a subtitle (file / content / url)           |
| `DELETE /api/v1/videos/{id}/subtitles/{subtitle_id}` | `videos:write` | Remove a subtitle (204 No Content)              |
| `POST /api/v1/videos/{id}/posters`                   | `videos:write` | Add a poster from a URL                         |
| `DELETE /api/v1/videos/{id}/posters/{poster_id}`     | `videos:write` | Remove a poster (204 No Content)                |
| `GET /api/v1/videos/imports/{id}`                    | `videos:read`  | Get video import job status                     |

***

### 4. Upload Signature (Single File)

Get pre-signed credentials to upload a video directly to AWS S3 using a single POST request. This enables a two-step upload process suitable for files under \~100MB.

> For files over 100MB, use the Multipart Upload API (Section 5. Multipart Upload) instead, which supports parallel and resumable uploads.

**Upload Flow:**

```
┌──────────┐     1. Get Signature      ┌──────────────┐
│  Client  │ ─────────────────────────▶│  Firework    │
│          │◀───────────────────────── │  API         │
└──────────┘   (signature + S3 key)    └──────────────┘
     │
     │  2. Upload file to S3
     │     (using signature)
     ▼
┌──────────┐
│  AWS S3  │
└──────────┘
     │
     │  3. Create video with S3 key
     ▼
┌──────────┐                           ┌──────────────┐
│  Client  │ ─────────────────────────▶│  Firework    │
│          │◀───────────────────────── │  API         │
└──────────┘      (video created)      └──────────────┘
```

**Endpoint**: `POST /api/v1/upload_signatures` **Authentication**: Bearer token required **Scope**: `videos:write`

#### 4.1. Request Headers

| Name            | Description                           | Required |
| --------------- | ------------------------------------- | -------- |
| `Authorization` | Bearer token: `Bearer {ACCESS_TOKEN}` | ✅        |
| `Content-Type`  | Must be `application/json`            | ✅        |

#### 4.2. Request Body

| Parameter    | Type   | Required | Description                                                  |
| ------------ | ------ | -------- | ------------------------------------------------------------ |
| `filename`   | string | ✅        | The name of the video file (e.g., `"my_video.mp4"`)          |
| `mime_type`  | string | ✅        | The MIME type of the video: `video/mp4` or `video/quicktime` |
| `channel_id` | string | ✅        | The encoded channel ID where the video will be uploaded      |

#### 4.3. Video Limits

The upload signature enforces the following limits:

| Limit                 | Value     |
| --------------------- | --------- |
| **Minimum file size** | 25 KB     |
| **Maximum file size** | 5 GB      |
| **Minimum duration**  | 3 seconds |
| **Maximum duration**  | 1 hour    |

#### 4.4. Upload Signature Response

**Success Response**: `201 Created`

| Field        | Type   | Description                                                                       |
| ------------ | ------ | --------------------------------------------------------------------------------- |
| `key`        | string | The S3 object key where the file will be stored. **Save this** for video creation |
| `post_url`   | string | The S3 URL to POST the file to                                                    |
| `policy`     | string | Base64-encoded policy document                                                    |
| `signature`  | string | The AWS Signature V4 value (`X-Amz-Signature`)                                    |
| `date`       | string | The signing date (`X-Amz-Date`), e.g., `"20250129T120000Z"`                       |
| `credential` | string | The AWS credential scope (`X-Amz-Credential`)                                     |
| `algorithm`  | string | Always `"AWS4-HMAC-SHA256"`                                                       |
| `acl`        | string | Always `"private"`                                                                |

#### 4.5. Upload Signature Error Responses

| Status Code        | Description                                                          |
| ------------------ | -------------------------------------------------------------------- |
| `400 Bad Request`  | Invalid `mime_type` - only `video/mp4` and `video/quicktime` allowed |
| `401 Unauthorized` | Invalid or missing authentication token                              |
| `403 Forbidden`    | Insufficient permissions to upload to the specified channel          |
| `404 Not Found`    | Channel not found                                                    |

#### 4.6. Uploading to S3

After receiving the signature, upload the file directly to S3 using a `multipart/form-data` POST request.

> ⚠️ **Important**: The form fields must be sent in the correct order, with the `file` field **last**.

**Required Form Fields (in order):**

| Field              | Value                             |
| ------------------ | --------------------------------- |
| `key`              | From signature response           |
| `acl`              | From signature response           |
| `X-Amz-Algorithm`  | From signature `algorithm`        |
| `X-Amz-Credential` | From signature `credential`       |
| `X-Amz-Date`       | From signature `date`             |
| `Policy`           | From signature `policy`           |
| `X-Amz-Signature`  | From signature `signature`        |
| `Content-Type`     | Same as request `mime_type`       |
| `file`             | The video file (**must be last**) |

**S3 Response:**

| Status | Description                                                               |
| ------ | ------------------------------------------------------------------------- |
| `204`  | Success - file uploaded                                                   |
| `400`  | Bad request - file size outside limits (< 25 KB or > 5 GB), or form error |
| `403`  | Forbidden - signature invalid or expired (expires after 60 min)           |

#### 4.7. Upload Signature Examples

**4.7.1. Get Signature Request**

```bash
curl -X POST "https://api.firework.com/api/v1/upload_signatures" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "filename": "product_demo.mp4",
    "mime_type": "video/mp4",
    "channel_id": "X6Xqd6W"
  }'
```

**4.7.2. Get Signature Response**

```json
{
  "key": "medias/business/AbCdEfG/channel/HiJkLmN/videos/public/original/1738152000-abcdefgh-product_demo.mp4",
  "post_url": "https://firework-assets.s3-accelerate.amazonaws.com",
  "policy": "eyJjb25kaXRpb25zIjpbeyJYLUFtei1BbGdvcml0aG0iOiJBV1M0LUhNQUMtU0hBMjU2In0sLi4u",
  "signature": "a1b2c3d4e5f6g7h8i9j0...",
  "date": "20250129T120000Z",
  "credential": "AKIAIOSFODNN7EXAMPLE/20250129/us-west-2/s3/aws4_request",
  "algorithm": "AWS4-HMAC-SHA256",
  "acl": "private"
}
```

**4.7.3. Upload to S3**

Use the `post_url` from the Get Signature response as the upload endpoint. Submit a `POST` request with the signature fields and your video file:

```bash
curl -X POST "{post_url}" \
  -F "key=medias/business/AbCdEfG/channel/HiJkLmN/videos/public/original/1738152000-abcdefgh-product_demo.mp4" \
  -F "acl=private" \
  -F "X-Amz-Algorithm=AWS4-HMAC-SHA256" \
  -F "X-Amz-Credential=AKIAIOSFODNN7EXAMPLE/20250129/us-west-2/s3/aws4_request" \
  -F "X-Amz-Date=20250129T120000Z" \
  -F "Policy=eyJjb25kaXRpb25zIjpbeyJYLUFtei1BbGdvcml0aG0iOiJBV1M0LUhNQUMtU0hBMjU2In0sLi4u" \
  -F "X-Amz-Signature=a1b2c3d4e5f6g7h8i9j0..." \
  -F "Content-Type=video/mp4" \
  -F "file=@/path/to/product_demo.mp4"
```

***

### 5. Multipart Upload

Upload large video files (100MB+) to AWS S3 using multipart upload. This splits the file into multiple parts that can be uploaded **in parallel** and **resumed** if a part fails, avoiding gateway timeouts.

> ⚠️ **AWS S3 Part Size Requirements**:
>
> * Each part (except the last) must be **≥ 5 MB** (5,242,880 bytes)
> * Last part can be any size
> * Maximum 100 parts per upload
> * If parts are too small, the complete step will fail with "Multipart upload failed"

**Multipart Upload Flow:**

```
┌──────────┐  1. Initiate Multipart     ┌──────────────┐
│  Client  │ ──────────────────────────▶│  Firework    │
│          │◀────────────────────────── │  API         │
└──────────┘  (key, upload_id, parts    └──────────────┘
               with presigned URLs)
     │
     │  2. Upload each part to S3
     │     (using presigned PUT URLs)
     │     ← can be done in parallel →
     ▼
┌──────────┐
│  AWS S3  │  ← returns ETag per part
└──────────┘
     │
     │  3. Complete multipart upload
     ▼
┌──────────┐                            ┌──────────────┐
│  Client  │ ──────────────────────────▶│  Firework    │
│          │◀────────────────────────── │  API         │
└──────────┘      (204 No Content)      └──────────────┘
     │
     │  4. Create video with S3 key
     ▼
┌──────────┐                            ┌──────────────┐
│  Client  │ ──────────────────────────▶│  Firework    │
│          │◀────────────────────────── │  API         │
└──────────┘      (video created)       └──────────────┘
```

#### 5.1. Initiate Multipart Upload

Start a multipart upload session. Returns an `upload_id` and presigned URLs for each part.

**Endpoint**: `POST /api/v1/upload_multipart/signatures` **Authentication**: Bearer token required **Scope**: `videos:write`

**5.1.1. Request Headers**

| Name            | Description                           | Required |
| --------------- | ------------------------------------- | -------- |
| `Authorization` | Bearer token: `Bearer {ACCESS_TOKEN}` | ✅        |
| `Content-Type`  | Must be `application/json`            | ✅        |

**5.1.2. Request Body**

| Parameter     | Type    | Required | Description                                                  |
| ------------- | ------- | -------- | ------------------------------------------------------------ |
| `filename`    | string  | ✅        | The name of the video file (e.g., `"my_video.mp4"`)          |
| `mime_type`   | string  | ✅        | The MIME type of the video: `video/mp4` or `video/quicktime` |
| `channel_id`  | string  | ✅        | The encoded channel ID where the video will be uploaded      |
| `parts_count` | integer | ✅        | Number of parts to split the file into (1-100)               |

> **Choosing `parts_count`:** Calculate based on your file size to ensure each part is ≥ 5 MB:
>
> * **Formula**: `parts_count = file_size_mb / 5` (round down)
> * **Example 1**: 100 MB file → max 20 parts (100 / 5 = 20)
> * **Example 2**: 500 MB file → max 100 parts (500 / 5 = 100)
> * **Example 3**: 15 MB file → max 3 parts (15 / 5 = 3)
> * **Important**: Each part (except last) must be ≥ 5 MB, or upload will fail

**5.1.3. Initiate Response**

**Success Response**: `201 Created`

| Field       | Type   | Description                                                                       |
| ----------- | ------ | --------------------------------------------------------------------------------- |
| `key`       | string | The S3 object key where the file will be stored. **Save this** for video creation |
| `upload_id` | string | The multipart upload session ID. Required for uploading parts and completion      |
| `parts`     | array  | Array of part objects, one per requested part                                     |

**Each element in `parts`:**

| Field               | Type    | Description                                          |
| ------------------- | ------- | ---------------------------------------------------- |
| `part`              | integer | The part number (1-based)                            |
| `signature`         | object  | Signature object containing the presigned PUT URL    |
| `signature.put_url` | string  | Presigned URL to PUT-upload this part directly to S3 |
| `signature.key`     | string  | The S3 object key                                    |

**5.1.4. Initiate Error Responses**

| Status Code        | Description                                                          |
| ------------------ | -------------------------------------------------------------------- |
| `400 Bad Request`  | Invalid `mime_type` - only `video/mp4` and `video/quicktime` allowed |
| `400 Bad Request`  | Invalid `parts_count` - must be between 1 and 100                    |
| `401 Unauthorized` | Invalid or missing authentication token                              |
| `403 Forbidden`    | Insufficient permissions to upload to the specified channel          |
| `404 Not Found`    | Channel not found                                                    |

#### 5.2. Upload Parts to S3

After initiating the multipart upload, upload each part directly to S3 using the presigned PUT URLs from the response.

> **Parts can be uploaded in parallel** for faster uploads. Each part returns an `ETag` header that you must save for the completion step.

**For each part:**

```bash
curl -X PUT "{part.signature.put_url}" \
  -H "Content-Type: video/mp4" \
  --data-binary @part_file
```

**S3 Response:**

| Status | Description                                                   |
| ------ | ------------------------------------------------------------- |
| `200`  | Success - part uploaded. **Save the `ETag` response header.** |
| `403`  | Forbidden - signature invalid or expired                      |

> **Important**: The `ETag` header value returned by S3 for each part is required for the completion step. It is typically a quoted MD5 hash, e.g., `"d41d8cd98f00b204e9800998ecf8427e"`.

#### 5.3. Complete Multipart Upload

After all parts have been uploaded to S3, call this endpoint to assemble them into the final file.

**Endpoint**: `POST /api/v1/upload_multipart/complete` **Authentication**: Bearer token required **Scope**: `videos:write`

**5.3.1. Request Headers**

| Name            | Description                           | Required |
| --------------- | ------------------------------------- | -------- |
| `Authorization` | Bearer token: `Bearer {ACCESS_TOKEN}` | ✅        |
| `Content-Type`  | Must be `application/json`            | ✅        |

**5.3.2. Request Body**

| Parameter   | Type   | Required | Description                                           |
| ----------- | ------ | -------- | ----------------------------------------------------- |
| `key`       | string | ✅        | The S3 key returned from the initiate step            |
| `upload_id` | string | ✅        | The upload session ID returned from the initiate step |
| `parts`     | array  | ✅        | Array of completed part objects (see below)           |

**Each element in `parts`:**

| Field  | Type    | Required | Description                                                              |
| ------ | ------- | -------- | ------------------------------------------------------------------------ |
| `part` | integer | ✅        | The part number (1-100, must match the initiate response, no duplicates) |
| `etag` | string  | ✅        | The ETag returned by S3 when the part was uploaded (non-empty)           |

> **Validation Rules**: The `parts` array must be non-empty, contain at most 100 elements, have no duplicate part numbers, and each `etag` must be a non-empty string.

**5.3.3. File Size Validation**

After assembly, the server validates the total file size against the same limits used for single-file uploads:

| Limit                 | Value |
| --------------------- | ----- |
| **Minimum file size** | 25 KB |
| **Maximum file size** | 5 GB  |

If the assembled file is outside these bounds, the server deletes the object from S3 and returns an error with a descriptive message: `"File too small (min 25KB)"` (400) or `"File too large (max 5GB)"` (413).

**5.3.4. Complete Response**

**Success Response**: `204 No Content`

No response body. The file has been assembled on S3 and is ready to be used with the Create Video API (Section 6. Create Video) using the `s3_key` parameter.

**5.3.5. Complete Error Responses**

| Status Code                    | Error Message                 | Description                                                                                                                                                     |
| ------------------------------ | ----------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `400 Bad Request`              |                               | Invalid or missing parameters (key, upload\_id, or parts), empty parts list, duplicate part numbers, invalid part numbers (must be 1-100), or empty etag values |
| `400 Bad Request`              | `"File too small (min 25KB)"` | Assembled file is below minimum size (25 KB)                                                                                                                    |
| `400 Bad Request`              | `"Multipart upload failed"`   | AWS rejected the upload (e.g., parts < 5 MB)                                                                                                                    |
| `401 Unauthorized`             |                               | Invalid or missing authentication token                                                                                                                         |
| `403 Forbidden`                |                               | Insufficient permissions                                                                                                                                        |
| `413 Request Entity Too Large` | `"File too large (max 5GB)"`  | Assembled file exceeds the maximum size (5 GB)                                                                                                                  |
| `500 Internal Server Error`    |                               | Unexpected server error during upload completion - retry the request                                                                                            |

> ⚠️ **Common Failure**: If you receive "Multipart upload failed" with a `400` status, it's usually because one or more parts (except the last) were smaller than 5 MB. Recalculate `parts_count` to ensure each part is at least 5 MB. A `500` status indicates a transient server issue - retry the request.

#### 5.4. Multipart Upload Examples

**5.4.1. Step 1: Initiate Multipart Upload**

```bash
curl -X POST "https://api.firework.com/api/v1/upload_multipart/signatures" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "filename": "large_product_demo.mp4",
    "mime_type": "video/mp4",
    "channel_id": "X6Xqd6W",
    "parts_count": 3
  }'
```

**Response:**

```json
{
  "key": "medias/business/AbCdEfG/channel/HiJkLmN/videos/public/original/1738152000-abcdefgh-large_product_demo.mp4",
  "upload_id": "VXBsb2FkIElEIGZvciBlbHZpbmcncyBt",
  "parts": [
    {
      "part": 1,
      "signature": {
        "put_url": "https://firework-assets.s3-accelerate.amazonaws.com/medias/...?uploadId=VXBsb2...&partNumber=1&X-Amz-Signature=abc123...",
        "key": "medias/business/AbCdEfG/channel/HiJkLmN/videos/public/original/1738152000-abcdefgh-large_product_demo.mp4"
      }
    },
    {
      "part": 2,
      "signature": {
        "put_url": "https://firework-assets.s3-accelerate.amazonaws.com/medias/...?uploadId=VXBsb2...&partNumber=2&X-Amz-Signature=def456...",
        "key": "medias/business/AbCdEfG/channel/HiJkLmN/videos/public/original/1738152000-abcdefgh-large_product_demo.mp4"
      }
    },
    {
      "part": 3,
      "signature": {
        "put_url": "https://firework-assets.s3-accelerate.amazonaws.com/medias/...?uploadId=VXBsb2...&partNumber=3&X-Amz-Signature=ghi789...",
        "key": "medias/business/AbCdEfG/channel/HiJkLmN/videos/public/original/1738152000-abcdefgh-large_product_demo.mp4"
      }
    }
  ]
}
```

**5.4.2. Step 2: Upload Parts to S3 (can be parallel)**

Split your file and upload each part using its presigned URL:

```bash
# Upload part 1
curl -X PUT "https://firework-assets.s3-accelerate.amazonaws.com/medias/...?uploadId=VXBsb2...&partNumber=1&X-Amz-Signature=abc123..." \
  -H "Content-Type: video/mp4" \
  --data-binary @part1.bin
# Save ETag from response headers: "a1b2c3d4..."

# Upload part 2 (in parallel)
curl -X PUT "https://firework-assets.s3-accelerate.amazonaws.com/medias/...?uploadId=VXBsb2...&partNumber=2&X-Amz-Signature=def456..." \
  -H "Content-Type: video/mp4" \
  --data-binary @part2.bin
# Save ETag from response headers: "e5f6g7h8..."

# Upload part 3 (in parallel)
curl -X PUT "https://firework-assets.s3-accelerate.amazonaws.com/medias/...?uploadId=VXBsb2...&partNumber=3&X-Amz-Signature=ghi789..." \
  -H "Content-Type: video/mp4" \
  --data-binary @part3.bin
# Save ETag from response headers: "i9j0k1l2..."
```

> **Tip**: To get the ETag from curl, use `-i` or `-D -` to include response headers in the output.

**5.4.3. Step 3: Complete Multipart Upload**

```bash
curl -X POST "https://api.firework.com/api/v1/upload_multipart/complete" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "key": "medias/business/AbCdEfG/channel/HiJkLmN/videos/public/original/1738152000-abcdefgh-large_product_demo.mp4",
    "upload_id": "VXBsb2FkIElEIGZvciBlbHZpbmcncyBt",
    "parts": [
      {"part": 1, "etag": "\"a1b2c3d4...\""},
      {"part": 2, "etag": "\"e5f6g7h8...\""},
      {"part": 3, "etag": "\"i9j0k1l2...\""}
    ]
  }'
```

**Response**: `204 No Content`

**5.4.4. Step 4: Create Video with S3 Key**

Use the `key` from the initiate step to create the video:

```bash
curl -X POST "https://api.firework.com/api/v1/videos" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "s3_key": "medias/business/AbCdEfG/channel/HiJkLmN/videos/public/original/1738152000-abcdefgh-large_product_demo.mp4",
    "channel_id": "X6Xqd6W",
    "caption": "Large Product Demo",
    "access": "public"
  }'
```

See Section 6. Create Video for full details on video creation.

***

### 6. Create Video

Upload a new video to the Firework platform. Supports direct file upload, video import from URL, creation from a pre-uploaded S3 key, and inline base64 upload for short clips.

**Endpoint**: `POST /api/v1/videos` **Authentication**: Bearer token required **Scope**: `videos:write` **Rate Limit**: 20 videos per 5 minutes per channel **Content Type**: `multipart/form-data` or `application/json`

> **`channel_id` is optional** when the token resolves to a single channel (an app token whose business has exactly one channel); otherwise it is required. This applies to every creation option below.

#### 6.1. Supported Video Files

* **MIME Types**: `video/mp4`, `video/quicktime`
* **File Extensions**: `.mp4`, `.mov`
* **Maximum Size**: 5GB

#### 6.2. Request Headers

| Name            | Description                                                                            | Required |
| --------------- | -------------------------------------------------------------------------------------- | -------- |
| `Authorization` | Bearer token: `Bearer {ACCESS_TOKEN}`                                                  | ✅        |
| `Content-Type`  | `multipart/form-data` (file upload) or `application/json` (URL import / S3 key upload) | ✅        |

#### 6.3. Request Body

**Option 1: File Upload (`multipart/form-data`)**

Upload the video file directly. Best for small files (under 100MB).

| Parameter  | Type   | Required | Description                                    |
| ---------- | ------ | -------- | ---------------------------------------------- |
| `metadata` | string | ✅        | JSON-encoded video metadata (see schema below) |
| `file`     | file   | ✅        | Video file to upload                           |

**Option 2: URL Import (`application/json`)**

Import a video from a publicly accessible URL. Supports two modes:

* **Sync mode** (default): The file is downloaded and uploaded to S3 within the request. Returns `201 Created` with a video object. Best for small files, but may time out on large files or slow URLs.
* **Async mode** (`"async": true`): Returns `202 Accepted` immediately with a video import tracking object. The file is downloaded and processed in the background. **Recommended for large files or unreliable URLs.**

> **How to choose:**

| Scenario                          | Mode           | Why                                       |
| --------------------------------- | -------------- | ----------------------------------------- |
| Small files (< 100 MB), fast URLs | Sync (default) | Simpler flow, video available immediately |
| Large files (100 MB+)             | Async          | Avoids gateway timeouts                   |
| Unreliable/slow URLs              | Async          | Background retry on failure               |
| Batch / fire-and-forget imports   | Async          | No need to wait for each video to finish  |

| Parameter                             | Type    | Required | Default | Description                                                 |
| ------------------------------------- | ------- | -------- | ------- | ----------------------------------------------------------- |
| `url`                                 | string  | ✅        | None    | Publicly accessible video URL (http or https)               |
| `async`                               | boolean | ❌        | `false` | Set to `true` for async processing (returns `202 Accepted`) |
| All other fields from metadata schema | -       | -        | -       | See Metadata Schema (Section 6.4)                           |

**URL Validation Rules:**

* Must be `http://` or `https://` scheme
* Must have a valid hostname (with at least one dot)
* Must have a path component
* The remote server must respond with a `Content-Type` header of `video/mp4`, `video/quicktime`, or `application/octet-stream`. When `application/octet-stream` is returned, the URL path must end with a video file extension (`.mp4` or `.mov`)
* The remote server must include a `Content-Length` header
* Maximum file size: 5 GB
* Video duration: 3 seconds to 1 hour

**Sync mode** (`"async"` omitted or `false`): Returns `201 Created` with a video object (same as file upload / S3 key). See Section 6.8.1.

**Async mode** (`"async": true`): Returns `202 Accepted` with a video import object. See Section 6.8.2.

> **Async Processing Flow:**
>
> 1. API validates the URL format, creates a video import job, and enqueues a background worker
> 2. Returns `202 Accepted` with the import job `id` and `status: "running"`
> 3. Background worker downloads the file to S3, creates the video record, and triggers transcoding. The `video_id` is populated at this point while `status` remains `"running"`
> 4. When transcoding completes: `status` becomes `"completed"` and `completed_at` is set
> 5. If processing fails (e.g., download error, invalid format, duration out of range): `status` becomes `"errored"`
>
> **Tracking progress:**
>
> * **Webhooks (recommended):** Configure `video_created`, `video_updated`, and `video_import_failed` [webhooks](/firework-for-developers/api/webhooks.md) to receive push notifications. The webhook payload includes the `import_id` so you can correlate events back to this import job.
> * **Polling:** Use `GET /api/v1/videos/imports/{id}` to poll for status. We recommend polling at 5–10 second intervals.

**Option 3: S3 Key Upload (`application/json`) - Recommended for Large Files**

Create a video from a file already uploaded to S3 via the Upload Signature API (Section 4. Upload Signature) or the Multipart Upload API (Section 5. Multipart Upload). **Recommended for files over 100MB** to avoid gateway timeouts.

| Parameter                             | Type   | Required | Default | Description                                   |
| ------------------------------------- | ------ | -------- | ------- | --------------------------------------------- |
| `s3_key`                              | string | ✅        | None    | The S3 key returned from Upload Signature API |
| All other fields from metadata schema | -      | -        | -       | See metadata schema section                   |

**Option 4: Inline Base64 Upload (`application/json`) - Short Clips Only**

Create a video by embedding the file bytes directly in the JSON body as base64. The server decodes and stores the bytes, so the client never needs S3 egress — but the payload is buffered whole and rides the JSON body limit.

> ⚠️ **Short clips only (≤ 5 MB decoded).** The decoded size must not exceed 5 MB (roughly 6.7 MB of base64 text). Larger payloads are rejected with `400`. For anything bigger, use URL import (Option 2) or the S3 key flow (Option 3).

| Parameter                             | Type   | Required | Default        | Description                                                                                 |
| ------------------------------------- | ------ | -------- | -------------- | ------------------------------------------------------------------------------------------- |
| `file_base64`                         | string | ✅        | None           | Base64-encoded mp4/mov bytes, ≤ 5 MB decoded. A `data:` URI prefix is accepted and stripped |
| `filename`                            | string | ❌        | `"upload.mp4"` | Optional filename for the stored object                                                     |
| All other fields from metadata schema | -      | -        | -              | See Metadata Schema (Section 6.4)                                                           |

#### 6.4. Metadata Schema

| Field                         | Type      | Required | Default       | Description                                          | Remarks                                                    |
| ----------------------------- | --------- | -------- | ------------- | ---------------------------------------------------- | ---------------------------------------------------------- |
| `channel_id`                  | string    | ✅        | None          | Encoded channel ID where video will be uploaded      |                                                            |
| `caption`                     | string    | ✅        | None          | Video title/caption                                  |                                                            |
| `description`                 | string    | ❌        | None          | Video description                                    |                                                            |
| `access`                      | string    | ❌        | `"public"`    | Video visibility: `"public"` or `"private"`          |                                                            |
| `archived_at`                 | string    | ❌        | None          | ISO 8601 timestamp when the video should be archived |                                                            |
| `audio_disabled`              | boolean   | ❌        | `false`       | Whether audio is disabled for the video              |                                                            |
| `hashtags`                    | string\[] | ❌        | `[]`          | Array of hashtag strings                             |                                                            |
| `business_store_id`           | string    | ❌        | use first one | Encoded business store ID                            | See products tagging rules                                 |
| `product_ids`                 | string\[] | ❌        | `[]`          | Array of product identifiers                         | See products tagging rules                                 |
| `variant_ids`                 | string\[] | ❌        | `[]`          | Array of product variant identifiers                 | See products tagging rules                                 |
| `custom_fields`               | object    | ❌        | `{}`          | Custom key-value metadata                            | See Metafields spec                                        |
| `display_social_attributions` | boolean   | ❌        | `false`       | Display social media attribution on video            | Requires `external_media` when `true`                      |
| `external_media`              | object    | ❌        | None          | Social media source metadata                         | See External Media Schema below                            |
| `poster_url`                  | string    | ❌        | None          | URL to a custom poster image                         | Set to `null` or `""` to remove. See Custom Poster section |
| `video_hidden`                | boolean   | ❌        | `false`       | Hide video from PDP (Product Detail Page)            | Applies to all product listings. See Product Tagging Rules |

#### 6.5. Custom Poster

The `poster_url` field allows you to specify a custom poster image for the video instead of using the auto-generated one.

**Supported Formats:**

* `jpg`, `png`

**Validation Rules:**

* Must be a valid, publicly accessible URL
* URL must have a valid image file extension (`.jpg`, `.png`)
* The image will be downloaded and stored on Firework's CDN

**Behavior:**

* When provided during video creation, the custom poster replaces the auto-generated poster
* When provided during video update, the custom poster replaces any existing poster
* To remove a custom poster, set `poster_url` to `null` or an empty string `""`
* Omit the field entirely to preserve the existing poster

**Examples:**

Set a custom poster:

```json
{
  "channel_id": "X6Xqd6W",
  "caption": "My Video",
  "poster_url": "https://example.com/my-custom-poster.jpg"
}
```

Remove the custom poster:

```json
{
  "channel_id": "X6Xqd6W",
  "caption": "My Video",
  "poster_url": null
}
```

#### 6.6 Product and Variant Identifiers:

The `product_ids` array accepts product identifiers that can be:

* Encoded Firework product ID
* External product ID
* External product unit ID
* Product unit GTIN
* Product unit SKU
* Product unit MPN
* Product unit barcode

The `variant_ids` array accepts product unit identifiers that can be:

* Encoded Firework product unit ID
* External product unit ID
* Product unit GTIN
* Product unit SKU
* Product unit MPN

**Product Tagging Rules:**

* **When `product_ids` is provided:**
  * While you can use product unit identifiers, they will only tag the related **products** to the video, not the **product units**
  * It will replace existing product and variant tags with the specified ones. For example, if a video is currently tagged with product A (`external ID "123"`) and product B (`external ID "234"`), using `product_ids: ["123", "567"]` will:
    * Keep product A tagged to the video
    * Untag product B from the video
    * Tag product C (`external ID "567"`) to the video
  * An empty array `product_ids: []` will untag all products and variants from the video
  * If a specified product identifier cannot be found in the business store, it will be **silently skipped**. Only the valid, resolvable products will be tagged to the video. No error is returned for unrecognized product IDs.
  * Duplicate product identifiers (including the same product referenced by different identifier types) will be silently deduplicated
  * The order of products will follow the order of the array. The sort ID will be set to match the order of the `product_ids` array.
* **When `variant_ids` is provided:**
  * It tags the specified product units (variants) to the video, not just the parent products
  * It has the same replace behavior as `product_ids`: providing `variant_ids` replaces existing product and variant tags with the specified variants, unless `product_ids` is also provided
  * If both `product_ids` and `variant_ids` are provided, the final product listing set is the resolved `product_ids` followed by the resolved `variant_ids`
  * An empty array `variant_ids: []` will untag all products and variants from the video when `product_ids` is not also provided
  * If a specified variant identifier cannot be found in the business store, it will be **silently skipped**
  * Duplicate variant identifiers (including the same variant referenced by different identifier types) will be silently deduplicated
  * The order of variants will follow the order of the array after any `product_ids` entries

**Examples:**

**Example 1: Tag products using external IDs**

```json
{
  "product_ids": ["SHOE-001", "SHIRT-123", "BAG-456"],
  "business_store_id": "encoded_store_id"
}
```

This will tag 3 products to the video in the specified order.

**Example 2: Tag products using Firework product IDs**

```json
{
  "product_ids": ["abc123", "def456"],
  "business_store_id": "encoded_store_id"
}
```

This will tag 2 products using their encoded Firework IDs.

**Example 3: Mix of identifier types**

```json
{
  "product_ids": ["SHOE-001", "071249656457", "abc123"],
  "business_store_id": "encoded_store_id"
}
```

This uses external ID, GTIN, and Firework ID respectively.

**Example 4: Replace existing product tags**

```json
// Video currently has products: ["OLD-001", "OLD-002"]
{
  "product_ids": ["NEW-001", "OLD-001"],
  "business_store_id": "encoded_store_id"
}
// Result: Video will have products ["NEW-001", "OLD-001"] only
// "OLD-002" gets untagged, "NEW-001" gets added
```

**Example 5: Untag all products**

```json
{
  "product_ids": [],
  "business_store_id": "encoded_store_id"
}
```

This removes all product tags from the video.

**Example 6: Using product unit identifiers**

```json
{
  "product_ids": ["UNIT-External-ID-1", "UNIT-External-ID-2"],
  "business_store_id": "encoded_store_id"
}
```

Even though these are unit IDs, only the related **products** get tagged to the video, not the **product units**.

**Example 7: Tag product variants explicitly**

```json
{
  "variant_ids": ["UNIT-External-ID-1", "UNIT-External-ID-2"],
  "business_store_id": "encoded_store_id"
}
```

This tags the specific product units (variants) to the video. The response includes their encoded Firework IDs in `variant_ids`.

**Example 8: Mix parent products and variants**

```json
{
  "product_ids": ["SHOE-001"],
  "variant_ids": ["UNIT-External-ID-1"],
  "business_store_id": "encoded_store_id"
}
```

This tags the parent product `SHOE-001` and the specific variant `UNIT-External-ID-1`.

**Example 9: Create video with hidden products and variants (hide from PDP)**

```json
{
  "product_ids": ["SHOE-001", "SHIRT-123"],
  "variant_ids": ["UNIT-External-ID-1"],
  "business_store_id": "encoded_store_id",
  "video_hidden": true
}
```

This tags products and variants to the video but hides it from the Product Detail Page.

**Example 10: Hide existing video from PDP (update without replacing products or variants)**

```json
{
  "video_hidden": true
}
```

When sent to `PATCH /api/v1/videos/{id}` without `product_ids` or `variant_ids`, this bulk-updates all existing product listings to be hidden.

**Example 11: Un-hide video on PDP**

```json
{
  "video_hidden": false
}
```

Sets all existing product listings back to visible on PDP.

* **`video_hidden` behavior:**
  * When `video_hidden` is provided with `product_ids` or `variant_ids`, all created/replaced product listings will be marked with the given value
  * When `video_hidden` is provided **without** `product_ids` or `variant_ids` (update only), it bulk-updates all existing product listings for the video
  * When `product_ids` or `variant_ids` are provided **without** `video_hidden`, existing product listings preserve their current `video_hidden` state; newly added products and variants default to `false` (visible)
  * The `hidden` field is **not** returned in the Video API response. It is returned in the [Product API](/firework-for-developers/api/products.md) (`GET /api/v1/products/:id/videos`), scoped to the queried product
  * Default is `false` (visible on PDP)
* **`business_store_id` behavior:**
  * Optional. If absent, the system will use the first business store of the business
  * If provided, the system will use the specified business store to find the product(s)

#### 6.7. External Media Schema

Used for social media attribution. Required when `display_social_attributions` is `true`.

| Field                | Type    | Required | Default | Description                                           |
| -------------------- | ------- | -------- | ------- | ----------------------------------------------------- |
| `source`             | string  | ✅        |         | Platform: `"tiktok"`, `"instagram"`, `"youtube"`, etc |
| `url`                | string  | ✅        |         | URL to the original social media post                 |
| `username`           | string  | ✅        |         | Creator's username/handle                             |
| `navigation_enabled` | boolean | ❌        | `true`  | Whether the URL is clickable in the player            |

**Example:**

```json
{
  "display_social_attributions": true,
  "external_media": {
    "source": "tiktok",
    "url": "https://www.tiktok.com/@username/video/1234567890",
    "username": "creator_handle",
    "navigation_enabled": false
  }
}
```

**Validation Rules:**

* When `display_social_attributions` is `true`, `external_media` must be provided with at least `source` and `url`
* For updates: validation passes if the video already has an existing `external_media` association

#### 6.8. Create Video Response

Two different response shapes depending on the creation method:

**6.8.1. File Upload / S3 Key / URL Import Sync Response (`201 Created`)**

For file upload, S3 key, inline base64, and URL import (sync mode), the video is created synchronously and returns immediately.

The response is the full **Video** object — the same shape returned by Get Video (Section 8), Update Video (Section 7), and the archive/unarchive/publish/unpublish/poster endpoints. See the Video Object reference (Section 8.3) for the complete field list, including `video_posters` and the CTA `action_*` fields.

| Field                         | Type      | Nullable | Description                                                                                              |
| ----------------------------- | --------- | -------- | -------------------------------------------------------------------------------------------------------- |
| `id`                          | string    | ❌        | Encoded video ID                                                                                         |
| `access`                      | string    | ❌        | Video visibility level (`"public"`, `"private"`, `"unlisted"`)                                           |
| `audio_disabled`              | boolean   | ❌        | Whether audio is disabled for the video (default: `false`)                                               |
| `caption`                     | string    | ✅        | Video title/caption                                                                                      |
| `description`                 | string    | ✅        | Video description                                                                                        |
| `hashtags`                    | string\[] | ❌        | Array of hashtag strings (empty if none provided)                                                        |
| `archived_at`                 | string    | ✅        | ISO 8601 timestamp when the video is/should be archived                                                  |
| `published_at`                | string    | ✅        | ISO 8601 publish time; `null` for an unpublished draft. A future value indicates a scheduled publication |
| `is_published`                | boolean   | ❌        | Whether the video is currently live (`published_at` set and not in the future). Computed at request time |
| `action_type`                 | string    | ✅        | Video CTA action type (e.g. `"shop_now"`, `"custom"`)                                                    |
| `action_type_translation`     | string    | ✅        | Translated CTA display label; for custom actions, this is the custom label                               |
| `action_url`                  | string    | ✅        | Video CTA destination URL                                                                                |
| `action_custom_label`         | string    | ✅        | Custom CTA label (used when `action_type` is `"custom"`)                                                 |
| `product_ids`                 | string\[] | ❌        | Array of Firework-encoded product IDs                                                                    |
| `variant_ids`                 | string\[] | ❌        | Array of Firework-encoded product variant IDs                                                            |
| `custom_fields`               | object    | ❌        | Custom key-value metadata                                                                                |
| `display_social_attributions` | boolean   | ✅        | Whether social attribution is displayed                                                                  |
| `external_media`              | object    | ✅        | Social media source metadata (see External Media Schema)                                                 |
| `thumbnail_url`               | string    | ✅        | CDN URL for the video thumbnail image (540x960)                                                          |
| `watch_url`                   | string    | ❌        | Web URL where a viewer can watch the video                                                               |
| `video_posters`               | array     | ❌        | Array of video poster images (empty if none). See Video Poster Schema (Section 8.3)                      |

**6.8.2. URL Import Async Response (`202 Accepted`)**

When `"async": true` is set, the video file is downloaded and processed **asynchronously**. The response returns a video import object — not a video. The video will be created in the background.

> **Tracking progress:** Configure [webhooks](/firework-for-developers/api/webhooks.md) to receive `video_created`, `video_updated`, and `video_import_failed` events (recommended), or poll with `GET /api/v1/videos/imports/{id}` at 5–10 second intervals. Webhook payloads include `import_id` to correlate events to this job.

| Field          | Type   | Nullable | Description                                                                                                   |
| -------------- | ------ | -------- | ------------------------------------------------------------------------------------------------------------- |
| `id`           | string | ❌        | Encoded import job ID. Use with `GET /api/v1/videos/imports/{id}`                                             |
| `status`       | string | ❌        | Import status (see Import Status Values below)                                                                |
| `video_id`     | string | ✅        | Encoded video ID. `null` initially, populated once the video record is created (before transcoding completes) |
| `created_at`   | string | ❌        | ISO 8601 timestamp when the import was created                                                                |
| `completed_at` | string | ✅        | ISO 8601 timestamp when the import completed. `null` while `running`                                          |

**Import Status Values**

| Status      | Description                                                                                                                                                |
| ----------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `running`   | Import is in progress: downloading URL, uploading to S3, creating video, or waiting for transcoding. `video_id` may already be populated during this phase |
| `completed` | Transcoding finished successfully. The video is fully ready                                                                                                |
| `errored`   | Import failed (download error, invalid format, duration out of range, transcode error)                                                                     |

#### 6.9. Create Video Error Responses

| Status Code                | Description                                                                                         |
| -------------------------- | --------------------------------------------------------------------------------------------------- |
| `400 Bad Request`          | Invalid request parameters, malformed JSON, unsupported file type, file size exceeds 5GB limit, etc |
| `401 Unauthorized`         | Invalid or missing authentication token                                                             |
| `403 Forbidden`            | Insufficient permissions                                                                            |
| `404 Not Found`            | Channel not found or membership not found                                                           |
| `422 Unprocessable Entity` | Video validation errors (e.g., caption too long, duration out of range, invalid values)             |
| `429 Too Many Requests`    | Rate limit exceeded (20 videos per 5 minutes per channel)                                           |

#### 6.10. Examples

**6.10.1. Option 1: File Upload (`multipart/form-data`)**

**CURL Request**

```bash
curl -X POST "https://api.firework.com/api/v1/videos" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: multipart/form-data" \
  -F 'metadata={
    "channel_id": "X6Xqd6W",
    "caption": "My Amazing Product Demo",
    "description": "Check out this awesome product in action!",
    "access": "public",
    "audio_disabled": false,
    "hashtags": ["product", "demo", "fashion"],
    "business_store_id": "encoded_store_id",
    "product_ids": ["product_id_1"],
    "variant_ids": ["variant_id_1"]
  }' \
  -F "file=@/path/to/your/video.mp4;type=video/mp4"
```

**HTTP Request**

```http
POST /api/v1/videos HTTP/1.1
Host: api.firework.com
Authorization: Bearer YOUR_ACCESS_TOKEN
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW
Content-Length: [calculated_length]

------WebKitFormBoundary7MA4YWxkTrZu0gW
Content-Disposition: form-data; name="metadata"
Content-Type: application/json

{
  "channel_id": "X6Xqd6W",
  "caption": "My Amazing Product Demo",
  "description": "Check out this awesome product in action!",
  "access": "public",
  "audio_disabled": false,
  "hashtags": ["product", "demo", "fashion"],
  "business_store_id": "encoded_store_id",
  "product_ids": ["product_id_1"],
  "variant_ids": ["variant_id_1"]
}
------WebKitFormBoundary7MA4YWxkTrZu0gW
Content-Disposition: form-data; name="file"; filename="demo_video.mp4"
Content-Type: video/mp4

[binary video file data]
------WebKitFormBoundary7MA4YWxkTrZu0gW--
```

**6.10.2. Option 2a: URL Import — Sync (default)**

```bash
curl -X POST "https://api.firework.com/api/v1/videos" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://example.com/videos/demo-video.mp4",
    "channel_id": "X6Xqd6W",
    "caption": "My Amazing Product Demo",
    "description": "Check out this awesome product in action!",
    "access": "public",
    "hashtags": ["product", "demo", "fashion"],
    "business_store_id": "encoded_store_id",
    "product_ids": ["product_id_1"],
    "variant_ids": ["variant_id_1"],
    "poster_url": "https://example.com/my-custom-poster.jpg"
  }'
```

**Response**: `201 Created` — same as file upload (see Section 6.10.4)

**6.10.2b. Option 2b: URL Import — Async**

```bash
curl -X POST "https://api.firework.com/api/v1/videos" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://example.com/videos/large-product-demo.mp4",
    "async": true,
    "channel_id": "X6Xqd6W",
    "caption": "My Amazing Product Demo",
    "description": "Check out this awesome product in action!",
    "access": "public",
    "hashtags": ["product", "demo", "fashion"],
    "business_store_id": "encoded_store_id",
    "product_ids": ["product_id_1"],
    "variant_ids": ["variant_id_1"],
    "poster_url": "https://example.com/my-custom-poster.jpg"
  }'
```

**Response**: `202 Accepted` — see Section 6.10.5

**6.10.3. Option 3: S3 Key Upload (`application/json`) - Recommended for Large Files**

First, get an upload signature and upload the file to S3 (see Section 4. Upload Signature or Section 5. Multipart Upload), then create the video with the S3 key.

**CURL Request**

```bash
curl -X POST "https://api.firework.com/api/v1/videos" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "s3_key": "medias/AbCdEfG/HiJkLmN/1738152000-abcdefgh/original/product_demo.mp4",
    "channel_id": "X6Xqd6W",
    "caption": "My Amazing Product Demo",
    "description": "Check out this awesome product in action!",
    "access": "public",
    "audio_disabled": false,
    "hashtags": ["product", "demo", "fashion"],
    "business_store_id": "encoded_store_id",
    "product_ids": ["product_id_1"],
    "variant_ids": ["variant_id_1"]
  }'
```

**HTTP Request**

```http
POST /api/v1/videos HTTP/1.1
Host: api.firework.com
Authorization: Bearer YOUR_ACCESS_TOKEN
Content-Type: application/json
Content-Length: [calculated_length]

{
  "s3_key": "medias/AbCdEfG/HiJkLmN/1738152000-abcdefgh/original/product_demo.mp4",
  "channel_id": "X6Xqd6W",
  "caption": "My Amazing Product Demo",
  "description": "Check out this awesome product in action!",
  "access": "public",
  "audio_disabled": false,
  "hashtags": ["product", "demo", "fashion"],
  "business_store_id": "encoded_store_id",
  "product_ids": ["product_id_1"],
  "variant_ids": ["variant_id_1"]
}
```

**6.10.3b. Option 4: Inline Base64 Upload (`application/json`) - Short Clips Only**

Embed the video bytes as base64 in the JSON body. Only for clips whose decoded size is ≤ 5 MB.

```bash
curl -X POST "https://api.firework.com/api/v1/videos" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "file_base64": "AAAAIGZ0eXBpc29tAAACAGlzb21pc28yYXZjMW1wNDE...",
    "filename": "short_clip.mp4",
    "channel_id": "X6Xqd6W",
    "caption": "My Amazing Product Demo",
    "access": "public",
    "hashtags": ["product", "demo"]
  }'
```

**Response**: `201 Created` — same as file upload (see Section 6.10.4)

**6.10.4. Success Response (File Upload / S3 Key) - `201 Created`**

```json
{
  "id": "encoded_video_id_xyz123",
  "access": "public",
  "audio_disabled": false,
  "caption": "My Amazing Product Demo",
  "description": "Check out this awesome product in action!",
  "hashtags": ["product", "demo", "fashion"],
  "archived_at": null,
  "published_at": "2025-01-29T12:00:00.000000Z",
  "is_published": true,
  "action_type": null,
  "action_type_translation": null,
  "action_url": null,
  "action_custom_label": null,
  "product_ids": ["product_id_1"],
  "variant_ids": ["variant_id_1"],
  "custom_fields": {},
  "display_social_attributions": false,
  "external_media": null,
  "thumbnail_url": "https://cdn.firework.com/medias/2026/3/18/abc123/540_960/thumb.jpg",
  "watch_url": "https://fw.tv/watch/encoded_video_id_xyz123",
  "video_posters": []
}
```

**6.10.5. Success Response (URL Import Async) - `202 Accepted`**

```json
{
  "id": "encoded_import_abc123",
  "status": "running",
  "video_id": null,
  "created_at": "2025-01-29T12:00:00.000000Z",
  "completed_at": null
}
```

***

### 7. Update Video

> **Update an existing video's data on the Firework platform.**

**Endpoint**: `PATCH /api/v1/videos/{video_id}` **Authentication**: Bearer token required **Scope**: `videos:write` **Content Type**: `application/json`

#### 7.1. Request Headers

| Name            | Description                           | Required |
| --------------- | ------------------------------------- | -------- |
| `Authorization` | Bearer token: `Bearer {ACCESS_TOKEN}` | ✅        |
| `Content-Type`  | Must be `application/json`            | ✅        |

#### 7.2. URL Parameters

| Parameter  | Type   | Required | Description               |
| ---------- | ------ | -------- | ------------------------- |
| `video_id` | string | ✅        | Firework encoded video ID |

#### 7.3. Request Body

| Parameter                     | Type      | Required | Description                                                                                                                                        |
| ----------------------------- | --------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------- |
| `caption`                     | string    | ❌        | Video title/caption                                                                                                                                |
| `description`                 | string    | ❌        | Video description                                                                                                                                  |
| `access`                      | string    | ❌        | Video visibility: `"public"` or `"private"`                                                                                                        |
| `archived_at`                 | string    | ❌        | ISO 8601 timestamp when the video should be archived                                                                                               |
| `audio_disabled`              | boolean   | ❌        | Whether audio is disabled for the video                                                                                                            |
| `hashtags`                    | string\[] | ❌        | Array of hashtag strings                                                                                                                           |
| `business_store_id`           | string    | ❌        | Encoded business store ID                                                                                                                          |
| `product_ids`                 | string\[] | ❌        | Array of product identifiers, see products tagging rules                                                                                           |
| `variant_ids`                 | string\[] | ❌        | Array of product variant identifiers, see products tagging rules                                                                                   |
| `custom_fields`               | object    | ❌        | Custom key-value metadata (replace mode)                                                                                                           |
| `display_social_attributions` | boolean   | ❌        | Display social media attribution on video                                                                                                          |
| `external_media`              | object    | ❌        | Social media source metadata (see External Media Schema)                                                                                           |
| `poster_url`                  | string    | ❌        | URL to custom poster. Set to `null` or `""` to remove. Omit to preserve.                                                                           |
| `video_hidden`                | boolean   | ❌        | Hide video from PDP. With `product_ids` or `variant_ids`: applies to all listings. Without: bulk-updates existing. Omit to preserve existing state |

#### 7.4. Update Video Response

**Success Response**: `200 OK`

Returns the full **Video** object — the same shape as Get Video (Section 8) and Create Video (Section 6.8). See the Video Object reference (Section 8.3) for the complete field list.

| Field                         | Type      | Nullable | Description                                                                                              |
| ----------------------------- | --------- | -------- | -------------------------------------------------------------------------------------------------------- |
| `id`                          | string    | ❌        | Encoded video ID                                                                                         |
| `access`                      | string    | ❌        | Video visibility level (`"public"`, `"private"`, `"unlisted"`)                                           |
| `audio_disabled`              | boolean   | ❌        | Whether audio is disabled for the video                                                                  |
| `caption`                     | string    | ✅        | Video title/caption                                                                                      |
| `description`                 | string    | ✅        | Video description                                                                                        |
| `hashtags`                    | string\[] | ❌        | Array of hashtag strings (empty if none provided)                                                        |
| `archived_at`                 | string    | ✅        | ISO 8601 timestamp when the video is/should be archived                                                  |
| `published_at`                | string    | ✅        | ISO 8601 publish time; `null` for an unpublished draft. A future value indicates a scheduled publication |
| `is_published`                | boolean   | ❌        | Whether the video is currently live (`published_at` set and not in the future). Computed at request time |
| `action_type`                 | string    | ✅        | Video CTA action type (e.g. `"shop_now"`, `"custom"`)                                                    |
| `action_type_translation`     | string    | ✅        | Translated CTA display label; for custom actions, this is the custom label                               |
| `action_url`                  | string    | ✅        | Video CTA destination URL                                                                                |
| `action_custom_label`         | string    | ✅        | Custom CTA label (used when `action_type` is `"custom"`)                                                 |
| `product_ids`                 | string\[] | ❌        | Array of Firework-encoded product IDs                                                                    |
| `variant_ids`                 | string\[] | ❌        | Array of Firework-encoded product variant IDs                                                            |
| `custom_fields`               | object    | ❌        | Custom key-value metadata                                                                                |
| `display_social_attributions` | boolean   | ✅        | Whether social attribution is displayed                                                                  |
| `external_media`              | object    | ✅        | Social media source metadata (see External Media Schema)                                                 |
| `thumbnail_url`               | string    | ✅        | CDN URL for the video thumbnail image (540x960)                                                          |
| `watch_url`                   | string    | ❌        | Web URL where a viewer can watch the video                                                               |
| `video_posters`               | array     | ❌        | Array of video poster images (empty if none). See Video Poster Schema (Section 8.3)                      |

#### 7.5. Update Video Error Responses

| Status Code                | Description                                                      |
| -------------------------- | ---------------------------------------------------------------- |
| `400 Bad Request`          | Invalid request parameters, malformed JSON, or validation errors |
| `401 Unauthorized`         | Invalid or missing authentication token                          |
| `403 Forbidden`            | Insufficient permissions                                         |
| `404 Not Found`            | Video not found                                                  |
| `422 Unprocessable Entity` | Video validation errors (e.g., caption too long, invalid values) |

#### 7.6. Update Examples

**7.6.1. CURL Request**

```bash
curl -X PATCH "https://api.firework.com/api/v1/videos/encoded_video_id_xyz123" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "caption": "Updated Amazing Product Demo",
    "description": "This updated description showcases even more amazing features!",
    "audio_disabled": true,
    "hashtags": ["updated", "product", "demo", "trending"],
    "business_store_id": "encoded_store_id",
    "product_ids": ["product_id_1", "SKU-67890"],
    "variant_ids": ["variant_id_1"]
  }'
```

**7.6.2. HTTP Request**

```http
PATCH /api/v1/videos/encoded_video_id_xyz123 HTTP/1.1
Host: api.firework.com
Authorization: Bearer YOUR_ACCESS_TOKEN
Content-Type: application/json
Content-Length: [calculated_length]

{
  "caption": "Updated Amazing Product Demo",
  "description": "This updated description showcases even more amazing features!",
  "audio_disabled": true,
  "hashtags": ["updated", "product", "demo", "trending"],
  "business_store_id": "encoded_store_id",
  "product_ids": ["product_id_1", "SKU-67890"],
  "variant_ids": ["variant_id_1"]
}
```

**7.6.3. Success Response**

```json
{
  "id": "encoded_video_id_xyz123",
  "access": "public",
  "audio_disabled": true,
  "caption": "Updated Amazing Product Demo",
  "description": "This updated description showcases even more amazing features!",
  "hashtags": ["updated", "product", "demo", "trending"],
  "archived_at": null,
  "published_at": "2025-01-29T12:00:00.000000Z",
  "is_published": true,
  "action_type": null,
  "action_type_translation": null,
  "action_url": null,
  "action_custom_label": null,
  "product_ids": ["product_id_1", "product_id_2"],
  "variant_ids": ["variant_id_1"],
  "custom_fields": {},
  "display_social_attributions": false,
  "external_media": null,
  "thumbnail_url": "https://cdn.firework.com/medias/2026/3/18/abc123/540_960/thumb.jpg",
  "watch_url": "https://fw.tv/watch/encoded_video_id_xyz123",
  "video_posters": []
}
```

**7.6.4. Remove Custom Poster**

To remove a custom poster from a video, set `poster_url` to `null` or an empty string:

```bash
curl -X PATCH "https://api.firework.com/api/v1/videos/encoded_video_id_xyz123" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "poster_url": null
  }'
```

Or with an empty string:

```bash
curl -X PATCH "https://api.firework.com/api/v1/videos/encoded_video_id_xyz123" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "poster_url": ""
  }'
```

***

### 8. Get Video

> **Retrieve a video's details from the Firework platform.**

**Endpoint**: `GET /api/v1/videos/{video_id}` **Authentication**: Bearer token required **Scope**: `videos:read` **Content Type**: N/A (no request body)

#### 8.1. Request Headers

| Name            | Description                           | Required |
| --------------- | ------------------------------------- | -------- |
| `Authorization` | Bearer token: `Bearer {ACCESS_TOKEN}` | ✅        |

#### 8.2. URL Parameters

| Parameter  | Type   | Required | Description               |
| ---------- | ------ | -------- | ------------------------- |
| `video_id` | string | ✅        | Firework encoded video ID |

#### 8.3. Get Video Response

**Success Response**: `200 OK`

> **Note**: This endpoint returns a video that has been created. For videos imported via URL, use `GET /api/v1/videos/imports/{id}` to track import status. Once the import completes, the `video_id` from the import response can be used with this endpoint.

**Video Object**

This is the canonical **Video** object shape. Every endpoint that returns a video — Create, Update, Get, archive, unarchive, publish, unpublish, and add-poster — returns exactly these fields.

| Field                         | Type      | Nullable | Description                                                                                              |
| ----------------------------- | --------- | -------- | -------------------------------------------------------------------------------------------------------- |
| `id`                          | string    | ❌        | Encoded video ID                                                                                         |
| `access`                      | string    | ❌        | Video visibility level (`"public"`, `"private"`, `"unlisted"`)                                           |
| `audio_disabled`              | boolean   | ❌        | Whether audio is disabled for the video                                                                  |
| `caption`                     | string    | ✅        | Video title/caption                                                                                      |
| `description`                 | string    | ✅        | Video description                                                                                        |
| `hashtags`                    | string\[] | ❌        | Array of hashtag strings (empty if none)                                                                 |
| `archived_at`                 | string    | ✅        | ISO 8601 timestamp when the video is/should be archived                                                  |
| `published_at`                | string    | ✅        | ISO 8601 publish time; `null` for an unpublished draft. A future value indicates a scheduled publication |
| `is_published`                | boolean   | ❌        | Whether the video is currently live (`published_at` set and not in the future). Computed at request time |
| `action_type`                 | string    | ✅        | Video CTA action type (e.g. `"shop_now"`, `"custom"`)                                                    |
| `action_type_translation`     | string    | ✅        | Translated CTA display label; for custom actions, this is the custom label                               |
| `action_url`                  | string    | ✅        | Video CTA destination URL                                                                                |
| `action_custom_label`         | string    | ✅        | Custom CTA label (used when `action_type` is `"custom"`)                                                 |
| `product_ids`                 | string\[] | ❌        | Array of Firework-encoded product IDs                                                                    |
| `variant_ids`                 | string\[] | ❌        | Array of Firework-encoded product variant IDs                                                            |
| `custom_fields`               | object    | ❌        | Custom key-value metadata                                                                                |
| `display_social_attributions` | boolean   | ✅        | Whether social attribution is displayed                                                                  |
| `external_media`              | object    | ✅        | Social media source metadata (see External Media Schema)                                                 |
| `thumbnail_url`               | string    | ✅        | CDN URL for the video thumbnail image (540x960)                                                          |
| `watch_url`                   | string    | ❌        | Web URL where a viewer can watch the video                                                               |
| `video_posters`               | array     | ❌        | Array of video poster images (empty if none). See Video Poster Schema                                    |

**Video Poster Schema**

Each object in the `video_posters` array contains:

| Field          | Type    | Nullable | Description                                                  |
| -------------- | ------- | -------- | ------------------------------------------------------------ |
| `id`           | string  | ❌        | Encoded poster ID (pass to `DELETE .../posters/{poster_id}`) |
| `url`          | string  | ❌        | CDN URL for the poster image                                 |
| `aspect_ratio` | string  | ✅        | Aspect ratio label (e.g. `"9:16"`, `"16:9"`, `"1:1"`)        |
| `format`       | string  | ❌        | Image format (`"jpg"`, `"webp"`, `"gif"`, `"png"`)           |
| `width`        | integer | ❌        | Image width in pixels                                        |
| `height`       | integer | ❌        | Image height in pixels                                       |

#### 8.4. Get Video Error Responses

| Status Code        | Description                             |
| ------------------ | --------------------------------------- |
| `401 Unauthorized` | Invalid or missing authentication token |
| `403 Forbidden`    | Insufficient permissions                |
| `404 Not Found`    | Video not found                         |

#### 8.5. Get Video Examples

**8.5.1. CURL Request**

```bash
curl -X GET "https://api.firework.com/api/v1/videos/encoded_video_id_xyz123" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"
```

**8.5.2. HTTP Request**

```http
GET /api/v1/videos/encoded_video_id_xyz123 HTTP/1.1
Host: api.firework.com
Authorization: Bearer YOUR_ACCESS_TOKEN
```

**8.5.3. Success Response**

```json
{
  "id": "encoded_video_id_xyz123",
  "access": "public",
  "audio_disabled": false,
  "caption": "My Amazing Product Demo",
  "description": "Check out this awesome product in action!",
  "hashtags": ["product", "demo", "fashion"],
  "archived_at": null,
  "published_at": "2025-01-29T12:00:00.000000Z",
  "is_published": true,
  "action_type": "shop_now",
  "action_type_translation": "Shop Now",
  "action_url": "https://example.com/products/shoe-001",
  "action_custom_label": null,
  "product_ids": ["product_id_1", "product_id_2"],
  "variant_ids": ["variant_id_1"],
  "custom_fields": {
    "creator_id": "2436372",
    "tracking_id": "390724V"
  },
  "display_social_attributions": true,
  "external_media": {
    "source": "tiktok",
    "url": "https://www.tiktok.com/@username/video/1234567890",
    "username": "creator_handle",
    "navigation_enabled": true
  },
  "thumbnail_url": "https://cdn.firework.com/medias/2026/3/18/abc123/540_960/thumb.jpg",
  "watch_url": "https://fw.tv/watch/encoded_video_id_xyz123",
  "video_posters": [
    {
      "id": "encoded_poster_id_1",
      "url": "https://cdn.firework.com/medias/2026/3/18/abc123/transcoded/poster-9x16.jpg",
      "aspect_ratio": "9:16",
      "format": "jpg",
      "width": 1080,
      "height": 1920
    },
    {
      "id": "encoded_poster_id_2",
      "url": "https://cdn.firework.com/medias/2026/3/18/abc123/transcoded/poster-16x9.jpg",
      "aspect_ratio": "16:9",
      "format": "jpg",
      "width": 1920,
      "height": 1080
    }
  ]
}
```

***

### 9. List Videos

> **List the videos in a channel.**

Returns a cursor-paginated list of a channel's videos. Results are ordered most-recently-created first (descending by ID) when no cursor is supplied.

**Endpoint**: `GET /api/v1/videos` **Authentication**: Bearer token required **Scope**: `videos:read` **Content Type**: N/A (no request body)

> **`channel_id` is optional** when the token resolves to a single channel (an app token whose business has exactly one channel); otherwise it is required.

#### 9.1. Request Headers

| Name            | Description                           | Required |
| --------------- | ------------------------------------- | -------- |
| `Authorization` | Bearer token: `Bearer {ACCESS_TOKEN}` | ✅        |

#### 9.2. Query Parameters

| Parameter    | Type    | Required | Description                                                                                     |
| ------------ | ------- | -------- | ----------------------------------------------------------------------------------------------- |
| `channel_id` | string  | ❌\*      | Encoded channel ID. Optional when the token's business has a single channel; otherwise required |
| `status`     | string  | ❌        | Filter by video status                                                                          |
| `access`     | string  | ❌        | Filter by access level (`public`, `private`, `unlisted`)                                        |
| `video_type` | string  | ❌        | Filter by video type (e.g. `live_stream`)                                                       |
| `archived`   | boolean | ❌        | Filter by archived state (`true` / `false`)                                                     |
| `published`  | boolean | ❌        | Filter by published state (`true` / `false`)                                                    |
| `after`      | string  | ❌        | Opaque cursor for the next page (from `pagination.cursor` or `links.next`). Ascending order     |
| `before`     | string  | ❌        | Opaque cursor for the previous page. Descending order                                           |
| `page_size`  | integer | ❌        | Items per page. Range 1–100. Default 10. Values above the max are clamped                       |

\* Required unless the token's business has exactly one channel, in which case that channel is used by default.

#### 9.3. List Videos Response

**Success Response**: `200 OK`

The response is an object containing the `videos` array plus a `links` object and a `pagination` object (per the public API cursor-pagination standard). Each element of `videos` is a full **Video** object — see the Video Object reference (Section 8.3).

| Field        | Type   | Nullable | Description                                                                                                                       |
| ------------ | ------ | -------- | --------------------------------------------------------------------------------------------------------------------------------- |
| `videos`     | array  | ❌        | Array of Video objects (see the Video Object reference, Section 8.3)                                                              |
| `links`      | object | ❌        | Pagination links (see below). Always present                                                                                      |
| `pagination` | object | ❌        | Pagination state (see below). Always present                                                                                      |
| `paging`     | object | ✅        | **Deprecated** legacy pagination object (`next`/`prev` URLs), retained during the migration window. Prefer `links` + `pagination` |

**`links` object** — this is a forward-only cursor feed, so only `next` is present (no `prev`):

| Key    | Type          | Nullable | Description                                                                                                                        |
| ------ | ------------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------- |
| `next` | string / null | ✅        | Relative path (including query string) to the next page, or `null` when there is no next page. Treat as opaque and follow verbatim |

**`pagination` object** — cursor strategy:

| Key        | Type          | Nullable | Description                                                                   |
| ---------- | ------------- | -------- | ----------------------------------------------------------------------------- |
| `cursor`   | string / null | ✅        | Opaque cursor for the next page (pass back as `after`); `null` when exhausted |
| `has_more` | boolean       | ❌        | `true` when another page is available now                                     |

#### 9.4. List Videos Error Responses

| Status Code        | Description                                           |
| ------------------ | ----------------------------------------------------- |
| `400 Bad Request`  | Invalid pagination cursor or query parameter          |
| `401 Unauthorized` | Invalid or missing authentication token               |
| `403 Forbidden`    | Insufficient permissions, or no access to the channel |
| `404 Not Found`    | Channel not found                                     |

#### 9.5. List Videos Examples

**9.5.1. CURL Request**

```bash
curl -X GET "https://api.firework.com/api/v1/videos?channel_id=X6Xqd6W&status=published&page_size=2" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"
```

**9.5.2. Success Response**

Each item in `videos` is a full Video object (abbreviated below — see the Video Object reference, Section 8.3, for all fields). A deprecated `paging` object is also present in the response body during the migration window; prefer `links` and `pagination`.

```json
{
  "videos": [
    {
      "id": "encoded_video_id_1",
      "access": "public",
      "audio_disabled": false,
      "caption": "Latest Product Demo",
      "description": null,
      "hashtags": ["product", "demo"],
      "archived_at": null,
      "published_at": "2025-01-29T12:00:00.000000Z",
      "is_published": true,
      "action_type": null,
      "action_type_translation": null,
      "action_url": null,
      "action_custom_label": null,
      "product_ids": ["product_id_1"],
      "variant_ids": [],
      "custom_fields": {},
      "display_social_attributions": false,
      "external_media": null,
      "thumbnail_url": "https://cdn.firework.com/medias/2026/3/18/v1/540_960/thumb.jpg",
      "watch_url": "https://fw.tv/watch/encoded_video_id_1",
      "video_posters": []
    },
    {
      "id": "encoded_video_id_2",
      "access": "public",
      "audio_disabled": false,
      "caption": "Earlier Product Demo",
      "description": null,
      "hashtags": [],
      "archived_at": null,
      "published_at": "2025-01-28T09:30:00.000000Z",
      "is_published": true,
      "action_type": null,
      "action_type_translation": null,
      "action_url": null,
      "action_custom_label": null,
      "product_ids": [],
      "variant_ids": [],
      "custom_fields": {},
      "display_social_attributions": false,
      "external_media": null,
      "thumbnail_url": "https://cdn.firework.com/medias/2026/3/17/v2/540_960/thumb.jpg",
      "watch_url": "https://fw.tv/watch/encoded_video_id_2",
      "video_posters": []
    }
  ],
  "links": {
    "next": "/api/v1/videos?channel_id=X6Xqd6W&status=published&page_size=2&before=eyJiZWZvcmVfaWQiOiJlbmNvZGVkX3ZpZGVvX2lkXzIifQ"
  },
  "pagination": {
    "cursor": "eyJiZWZvcmVfaWQiOiJlbmNvZGVkX3ZpZGVvX2lkXzIifQ",
    "has_more": true
  }
}
```

> **Following pages:** treat `links.next` as opaque and request it verbatim (it already carries the filters and cursor), or pass `pagination.cursor` back as the `after` query parameter. When `links.next` is `null` (and `pagination.cursor` is `null`), you have reached the last page.

***

### 10. Delete Video

> **Delete a video from the Firework platform.**

Soft-deletes the video (it is marked deleted, not hard-deleted).

**Endpoint**: `DELETE /api/v1/videos/{video_id}` **Authentication**: Bearer token required **Scope**: `videos:write`

#### 10.1. Request Headers

| Name            | Description                           | Required |
| --------------- | ------------------------------------- | -------- |
| `Authorization` | Bearer token: `Bearer {ACCESS_TOKEN}` | ✅        |

#### 10.2. URL Parameters

| Parameter  | Type   | Required | Description               |
| ---------- | ------ | -------- | ------------------------- |
| `video_id` | string | ✅        | Firework encoded video ID |

#### 10.3. Delete Video Response

**Success Response**: `204 No Content`

No response body.

#### 10.4. Delete Video Error Responses

| Status Code        | Description                             |
| ------------------ | --------------------------------------- |
| `401 Unauthorized` | Invalid or missing authentication token |
| `403 Forbidden`    | Insufficient permissions                |
| `404 Not Found`    | Video not found                         |

#### 10.5. Delete Video Example

```bash
curl -X DELETE "https://api.firework.com/api/v1/videos/encoded_video_id_xyz123" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"
```

**Response**: `204 No Content`

***

### 11. Archive and Unarchive Video

> **Archive or restore a video.**

Archiving sets the video's `archived_at` timestamp to the current time; unarchiving clears it (`archived_at` becomes `null`). Both return the full updated **Video** object.

**Endpoints**:

* `POST /api/v1/videos/{video_id}/archive`
* `POST /api/v1/videos/{video_id}/unarchive`

**Authentication**: Bearer token required **Scope**: `videos:write` **Content Type**: N/A (no request body)

#### 11.1. Request Headers

| Name            | Description                           | Required |
| --------------- | ------------------------------------- | -------- |
| `Authorization` | Bearer token: `Bearer {ACCESS_TOKEN}` | ✅        |

#### 11.2. URL Parameters

| Parameter  | Type   | Required | Description               |
| ---------- | ------ | -------- | ------------------------- |
| `video_id` | string | ✅        | Firework encoded video ID |

#### 11.3. Response

**Success Response**: `200 OK`

Returns the full **Video** object (see the Video Object reference, Section 8.3). After archiving, `archived_at` is set to the time of the request; after unarchiving, `archived_at` is `null`.

#### 11.4. Error Responses

| Status Code        | Description                             |
| ------------------ | --------------------------------------- |
| `401 Unauthorized` | Invalid or missing authentication token |
| `403 Forbidden`    | Insufficient permissions                |
| `404 Not Found`    | Video not found                         |

#### 11.5. Examples

Archive a video:

```bash
curl -X POST "https://api.firework.com/api/v1/videos/encoded_video_id_xyz123/archive" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"
```

Unarchive a video:

```bash
curl -X POST "https://api.firework.com/api/v1/videos/encoded_video_id_xyz123/unarchive" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"
```

**Response (archive)**: `200 OK` — the Video object with `archived_at` populated:

```json
{
  "id": "encoded_video_id_xyz123",
  "access": "public",
  "audio_disabled": false,
  "caption": "My Amazing Product Demo",
  "description": "Check out this awesome product in action!",
  "hashtags": ["product", "demo"],
  "archived_at": "2025-01-29T12:05:00.000000Z",
  "published_at": "2025-01-29T12:00:00.000000Z",
  "is_published": true,
  "action_type": null,
  "action_type_translation": null,
  "action_url": null,
  "action_custom_label": null,
  "product_ids": ["product_id_1"],
  "variant_ids": [],
  "custom_fields": {},
  "display_social_attributions": false,
  "external_media": null,
  "thumbnail_url": "https://cdn.firework.com/medias/2026/3/18/abc123/540_960/thumb.jpg",
  "watch_url": "https://fw.tv/watch/encoded_video_id_xyz123",
  "video_posters": []
}
```

***

### 12. Publish and Unpublish Video

> **Publish a video immediately, schedule it for later, or revert it to a draft.**

**Endpoints**:

* `POST /api/v1/videos/{video_id}/publish`
* `POST /api/v1/videos/{video_id}/unpublish`

**Authentication**: Bearer token required **Scope**: `videos:write` **Content Type**: `application/json`

#### 12.1. Publish

Publishes the video. The behavior depends on the optional `published_at` field in the request body:

* **Omit the body** (or send `{}` / `published_at: null`) → the video is published **immediately**.
* **`published_at` is a future time** → the video is **scheduled**. It becomes visible automatically once the time passes, with no further API call. `is_published` stays `false` until then.
* **`published_at` is in the past, or more than 28 days in the future** → `422 Unprocessable Entity`. Scheduling is capped at **28 days** from now.

Send the request with `Content-Type: application/json`.

**12.1.1. Request Headers**

| Name            | Description                           | Required |
| --------------- | ------------------------------------- | -------- |
| `Authorization` | Bearer token: `Bearer {ACCESS_TOKEN}` | ✅        |
| `Content-Type`  | Must be `application/json`            | ✅        |

**12.1.2. URL Parameters**

| Parameter  | Type   | Required | Description               |
| ---------- | ------ | -------- | ------------------------- |
| `video_id` | string | ✅        | Firework encoded video ID |

**12.1.3. Request Body**

| Parameter      | Type   | Required | Description                                                                                                                                           |
| -------------- | ------ | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- |
| `published_at` | string | ❌        | ISO 8601 time to publish. A future time (within 28 days) schedules the video; a time in the past or beyond 28 days returns `422`. Omit to publish now |

**12.1.4. Response**

**Success Response**: `200 OK`

Returns the full **Video** object (see the Video Object reference, Section 8.3). For an immediate publish, `published_at` is set to the request time and `is_published` is `true`. For a scheduled publish, `published_at` is the future time and `is_published` is `false` until that time passes.

**12.1.5. Publish Error Responses**

| Status Code                | Description                                                                                  |
| -------------------------- | -------------------------------------------------------------------------------------------- |
| `400 Bad Request`          | Malformed request (e.g. invalid JSON)                                                        |
| `401 Unauthorized`         | Invalid or missing authentication token                                                      |
| `403 Forbidden`            | Insufficient permissions                                                                     |
| `404 Not Found`            | Video not found                                                                              |
| `422 Unprocessable Entity` | `published_at` is not a valid ISO 8601 datetime, is in the past, or is more than 28 days out |

**12.1.6. Publish Examples**

Publish immediately:

```bash
curl -X POST "https://api.firework.com/api/v1/videos/encoded_video_id_xyz123/publish" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json"
```

Schedule for a future time (within 28 days):

```bash
curl -X POST "https://api.firework.com/api/v1/videos/encoded_video_id_xyz123/publish" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "published_at": "2025-02-10T09:00:00Z"
  }'
```

**Response (scheduled)**: `200 OK`

```json
{
  "id": "encoded_video_id_xyz123",
  "access": "public",
  "audio_disabled": false,
  "caption": "My Amazing Product Demo",
  "description": null,
  "hashtags": [],
  "archived_at": null,
  "published_at": "2025-02-10T09:00:00.000000Z",
  "is_published": false,
  "action_type": null,
  "action_type_translation": null,
  "action_url": null,
  "action_custom_label": null,
  "product_ids": [],
  "variant_ids": [],
  "custom_fields": {},
  "display_social_attributions": false,
  "external_media": null,
  "thumbnail_url": "https://cdn.firework.com/medias/2026/3/18/abc123/540_960/thumb.jpg",
  "watch_url": "https://fw.tv/watch/encoded_video_id_xyz123",
  "video_posters": []
}
```

#### 12.2. Unpublish

Reverts the video to an unpublished draft by clearing `published_at`. The video is hidden from feeds and product lookups until it is published again. Takes no request body.

**12.2.1. Request Headers**

| Name            | Description                           | Required |
| --------------- | ------------------------------------- | -------- |
| `Authorization` | Bearer token: `Bearer {ACCESS_TOKEN}` | ✅        |

**12.2.2. URL Parameters**

| Parameter  | Type   | Required | Description               |
| ---------- | ------ | -------- | ------------------------- |
| `video_id` | string | ✅        | Firework encoded video ID |

**12.2.3. Response**

**Success Response**: `200 OK`

Returns the full **Video** object (see the Video Object reference, Section 8.3) with `published_at` set to `null` and `is_published` set to `false`.

**12.2.4. Unpublish Error Responses**

| Status Code        | Description                             |
| ------------------ | --------------------------------------- |
| `401 Unauthorized` | Invalid or missing authentication token |
| `403 Forbidden`    | Insufficient permissions                |
| `404 Not Found`    | Video not found                         |

**12.2.5. Unpublish Example**

```bash
curl -X POST "https://api.firework.com/api/v1/videos/encoded_video_id_xyz123/unpublish" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"
```

***

### 13. Video Subtitles

> **Add or remove subtitle tracks on a video.**

#### 13.1. Add Subtitle

Adds a subtitle track (`.vtt` or `.srt`, max 5MB). The subtitle source can be supplied in **three ways**:

1. **Multipart file** (`multipart/form-data`) — upload the subtitle file directly.
2. **Inline content** (`application/json`) — pass the raw `.vtt`/`.srt` text in the `content` field.
3. **URL** (`application/json`) — pass a public `url` the server downloads.

**Endpoint**: `POST /api/v1/videos/{video_id}/subtitles` **Authentication**: Bearer token required **Scope**: `videos:write` **Content Type**: `multipart/form-data` or `application/json`

**13.1.1. URL Parameters**

| Parameter  | Type   | Required | Description               |
| ---------- | ------ | -------- | ------------------------- |
| `video_id` | string | ✅        | Firework encoded video ID |

**13.1.2. Request Body**

**Option 1: Multipart file (`multipart/form-data`)**

| Parameter  | Type    | Required | Description                                               |
| ---------- | ------- | -------- | --------------------------------------------------------- |
| `language` | string  | ✅        | BCP-47 language code, e.g. `en` or `en-US`                |
| `file`     | file    | ✅        | Subtitle file (`.vtt` or `.srt`, max 5MB)                 |
| `is_cc`    | boolean | ❌        | Whether this is a closed-captions track (default `false`) |

**Option 2: Inline content (`application/json`)**

| Parameter  | Type    | Required | Description                                                 |
| ---------- | ------- | -------- | ----------------------------------------------------------- |
| `language` | string  | ✅        | BCP-47 language code, e.g. `en` or `en-US`                  |
| `content`  | string  | ✅        | The subtitle file contents (`.vtt` or `.srt` text, max 5MB) |
| `is_cc`    | boolean | ❌        | Whether this is a closed-captions track (default `false`)   |

**Option 3: URL (`application/json`)**

| Parameter  | Type    | Required | Description                                               |
| ---------- | ------- | -------- | --------------------------------------------------------- |
| `language` | string  | ✅        | BCP-47 language code, e.g. `en` or `en-US`                |
| `url`      | string  | ✅        | HTTP(S) URL of a `.vtt` or `.srt` file (max 5MB)          |
| `is_cc`    | boolean | ❌        | Whether this is a closed-captions track (default `false`) |

**13.1.3. Add Subtitle Response**

**Success Response**: `201 Created`

| Field      | Type    | Nullable | Description                             |
| ---------- | ------- | -------- | --------------------------------------- |
| `id`       | string  | ❌        | Encoded subtitle ID                     |
| `language` | string  | ❌        | BCP-47 language code                    |
| `is_cc`    | boolean | ❌        | Whether this is a closed-captions track |

**13.1.4. Add Subtitle Error Responses**

| Status Code                | Description                                                                                  |
| -------------------------- | -------------------------------------------------------------------------------------------- |
| `400 Bad Request`          | Missing required fields, unsupported file (expected `.vtt`/`.srt`), or file/content over 5MB |
| `401 Unauthorized`         | Invalid or missing authentication token                                                      |
| `403 Forbidden`            | Insufficient permissions                                                                     |
| `404 Not Found`            | Video not found                                                                              |
| `422 Unprocessable Entity` | Subtitle validation error                                                                    |

**13.1.5. Add Subtitle Examples**

Multipart file upload:

```bash
curl -X POST "https://api.firework.com/api/v1/videos/encoded_video_id_xyz123/subtitles" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -F "language=en-US" \
  -F "is_cc=false" \
  -F "file=@/path/to/captions.vtt;type=text/vtt"
```

Inline content (JSON):

```bash
curl -X POST "https://api.firework.com/api/v1/videos/encoded_video_id_xyz123/subtitles" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "language": "en-US",
    "content": "WEBVTT\n\n00:00:00.000 --> 00:00:02.000\nHello world"
  }'
```

From a URL (JSON):

```bash
curl -X POST "https://api.firework.com/api/v1/videos/encoded_video_id_xyz123/subtitles" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "language": "en-US",
    "url": "https://example.com/captions/en.vtt"
  }'
```

**Response**: `201 Created`

```json
{
  "id": "encoded_subtitle_id_abc",
  "language": "en-US",
  "is_cc": false
}
```

#### 13.2. Delete Subtitle

Removes a subtitle track from a video.

**Endpoint**: `DELETE /api/v1/videos/{video_id}/subtitles/{subtitle_id}` **Authentication**: Bearer token required **Scope**: `videos:write`

**13.2.1. URL Parameters**

| Parameter     | Type   | Required | Description                  |
| ------------- | ------ | -------- | ---------------------------- |
| `video_id`    | string | ✅        | Firework encoded video ID    |
| `subtitle_id` | string | ✅        | Firework encoded subtitle ID |

**13.2.2. Delete Subtitle Response**

**Success Response**: `204 No Content`

No response body.

**13.2.3. Delete Subtitle Error Responses**

| Status Code        | Description                                                 |
| ------------------ | ----------------------------------------------------------- |
| `401 Unauthorized` | Invalid or missing authentication token                     |
| `403 Forbidden`    | Insufficient permissions                                    |
| `404 Not Found`    | Video or subtitle not found (or subtitle not on this video) |

**13.2.4. Delete Subtitle Example**

```bash
curl -X DELETE "https://api.firework.com/api/v1/videos/encoded_video_id_xyz123/subtitles/encoded_subtitle_id_abc" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"
```

**Response**: `204 No Content`

***

### 14. Video Posters

> **Add or remove poster images on a video.**

#### 14.1. Add Poster

Adds a poster image to the video by downloading it from a URL. The image is stored on Firework's CDN and appended to the video's `video_posters`.

**Endpoint**: `POST /api/v1/videos/{video_id}/posters` **Authentication**: Bearer token required **Scope**: `videos:write` **Content Type**: `application/json`

> **Poster vs. `poster_url` on create/update:** `POST .../posters` **adds** a poster to the video's poster set and returns the video. The `poster_url` field on Create/Update Video *replaces* the video's posters instead. Use whichever fits your flow.

**14.1.1. URL Parameters**

| Parameter  | Type   | Required | Description               |
| ---------- | ------ | -------- | ------------------------- |
| `video_id` | string | ✅        | Firework encoded video ID |

**14.1.2. Request Body**

| Parameter | Type   | Required | Description                                                                                           |
| --------- | ------ | -------- | ----------------------------------------------------------------------------------------------------- |
| `url`     | string | ✅        | HTTP(S) URL of a poster image. Must end in a valid image extension (`.jpg`, `.jpeg`, `.png`, `.webp`) |

**14.1.3. Add Poster Response**

**Success Response**: `201 Created`

Returns the full **Video** object (see the Video Object reference, Section 8.3) with the new poster included in `video_posters`. Each poster has an `id` you can use to delete it (see Video Poster Schema, Section 8.3).

**14.1.4. Add Poster Error Responses**

| Status Code                | Description                                                                                                 |
| -------------------------- | ----------------------------------------------------------------------------------------------------------- |
| `400 Bad Request`          | Missing `url`, invalid/unsupported poster format, missing file extension, or the image could not be fetched |
| `401 Unauthorized`         | Invalid or missing authentication token                                                                     |
| `403 Forbidden`            | Insufficient permissions                                                                                    |
| `404 Not Found`            | Video not found                                                                                             |
| `422 Unprocessable Entity` | Poster validation error                                                                                     |

**14.1.5. Add Poster Example**

```bash
curl -X POST "https://api.firework.com/api/v1/videos/encoded_video_id_xyz123/posters" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://example.com/posters/my-poster.jpg"
  }'
```

**Response**: `201 Created` — the Video object with the new poster in `video_posters`:

```json
{
  "id": "encoded_video_id_xyz123",
  "access": "public",
  "audio_disabled": false,
  "caption": "My Amazing Product Demo",
  "description": null,
  "hashtags": [],
  "archived_at": null,
  "published_at": "2025-01-29T12:00:00.000000Z",
  "is_published": true,
  "action_type": null,
  "action_type_translation": null,
  "action_url": null,
  "action_custom_label": null,
  "product_ids": [],
  "variant_ids": [],
  "custom_fields": {},
  "display_social_attributions": false,
  "external_media": null,
  "thumbnail_url": "https://cdn.firework.com/medias/2026/3/18/abc123/540_960/thumb.jpg",
  "watch_url": "https://fw.tv/watch/encoded_video_id_xyz123",
  "video_posters": [
    {
      "id": "encoded_poster_id_new",
      "url": "https://cdn.firework.com/medias/.../posters/my-poster.jpg",
      "aspect_ratio": "9:16",
      "format": "jpg",
      "width": 1080,
      "height": 1920
    }
  ]
}
```

#### 14.2. Delete Poster

Removes a poster image from a video. Get the `poster_id` from the `video_posters[].id` field of any Video response.

**Endpoint**: `DELETE /api/v1/videos/{video_id}/posters/{poster_id}` **Authentication**: Bearer token required **Scope**: `videos:write`

**14.2.1. URL Parameters**

| Parameter   | Type   | Required | Description                |
| ----------- | ------ | -------- | -------------------------- |
| `video_id`  | string | ✅        | Firework encoded video ID  |
| `poster_id` | string | ✅        | Firework encoded poster ID |

**14.2.2. Delete Poster Response**

**Success Response**: `204 No Content`

No response body.

**14.2.3. Delete Poster Error Responses**

| Status Code        | Description                                             |
| ------------------ | ------------------------------------------------------- |
| `401 Unauthorized` | Invalid or missing authentication token                 |
| `403 Forbidden`    | Insufficient permissions                                |
| `404 Not Found`    | Video or poster not found (or poster not on this video) |

**14.2.4. Delete Poster Example**

```bash
curl -X DELETE "https://api.firework.com/api/v1/videos/encoded_video_id_xyz123/posters/encoded_poster_id_new" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"
```

**Response**: `204 No Content`

***

### 15. Get Video Import Status

> **Track the status of an async URL import.**

Use this endpoint to check the progress of a video import initiated via the URL import method. Once the import completes successfully, the response includes the `video_id` which can be used with the Get Video (Section 8) and Update Video (Section 7) endpoints.

> **Tip:** For real-time notifications instead of polling, configure [webhooks](/firework-for-developers/api/webhooks.md). The `video_created`, `video_updated`, and `video_import_failed` events include `import_id` so you can correlate events back to this import job.

**Endpoint**: `GET /api/v1/videos/imports/{id}` **Authentication**: Bearer token required **Scope**: `videos:read`

#### 15.1. Request Headers

| Name            | Description                           | Required |
| --------------- | ------------------------------------- | -------- |
| `Authorization` | Bearer token: `Bearer {ACCESS_TOKEN}` | ✅        |

#### 15.2. URL Parameters

| Parameter | Type   | Required | Description                                   |
| --------- | ------ | -------- | --------------------------------------------- |
| `id`      | string | ✅        | Encoded import job ID (from the 202 response) |

#### 15.3. Get Import Status Response

**Success Response**: `200 OK`

| Field          | Type   | Nullable | Description                                                                                                       |
| -------------- | ------ | -------- | ----------------------------------------------------------------------------------------------------------------- |
| `id`           | string | ❌        | Encoded import job ID                                                                                             |
| `status`       | string | ❌        | Import status: `"running"`, `"completed"`, or `"errored"`                                                         |
| `video_id`     | string | ✅        | Encoded video ID. `null` initially, populated once the video record is created (may appear while still `running`) |
| `created_at`   | string | ❌        | ISO 8601 timestamp when the import was created                                                                    |
| `completed_at` | string | ✅        | ISO 8601 timestamp when the import completed. `null` while `running`                                              |

#### 15.4. Get Import Status Error Responses

| Status Code        | Description                             |
| ------------------ | --------------------------------------- |
| `401 Unauthorized` | Invalid or missing authentication token |
| `403 Forbidden`    | Insufficient permissions                |
| `404 Not Found`    | Import job not found                    |

#### 15.5. Get Import Status Examples

**15.5.1. CURL Request**

```bash
curl -X GET "https://api.firework.com/api/v1/videos/imports/encoded_import_abc123" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"
```

**15.5.2. Response (Running)**

```json
{
  "id": "encoded_import_abc123",
  "status": "running",
  "video_id": null,
  "created_at": "2025-01-29T12:00:00.000000Z",
  "completed_at": null
}
```

**15.5.3. Response (Completed)**

```json
{
  "id": "encoded_import_abc123",
  "status": "completed",
  "video_id": "encoded_video_id_xyz123",
  "created_at": "2025-01-29T12:00:00.000000Z",
  "completed_at": "2025-01-29T12:02:30.000000Z"
}
```

> **Next step**: Use the `video_id` with `GET /api/v1/videos/{video_id}` to get the full video details.

**15.5.4. Response (Errored)**

```json
{
  "id": "encoded_import_abc123",
  "status": "errored",
  "video_id": null,
  "created_at": "2025-01-29T12:00:00.000000Z",
  "completed_at": null
}
```

***

### 16. Custom Fields Extension

The Video API supports custom metadata through the `custom_fields` parameter. This allows you to attach arbitrary key-value pairs to videos for tracking and analytics purposes.

**Key Points**:

* **Replace Mode**: Providing `custom_fields` replaces ALL existing custom fields
* **Preserve Existing**: Omit `custom_fields` from request to keep existing values
* **Clear All**: Use `custom_fields: {}` to remove all custom fields
* **Validation**: Keys must match `^[a-z0-9_]{1,255}$`, values max 1024 characters

**Example with Custom Fields**:

```bash
curl -X PATCH "https://api.firework.com/api/v1/videos/xyz123" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "caption": "Product Demo",
    "custom_fields": {
      "creator_id": "2436372",
      "publisher_id": "3264535",
      "tracking_id": "390724V"
    }
  }'
```

***
