> 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/ai-content.md).

# AI Content

### 1. Overview

The Firework AI Content API exposes two related workflows:

* **Video-product matching** scores products against an approved Firework video, then lets an integration accept the desired matches.
* **AI content curation** discovers social videos for a channel, manages discovery settings and creator whitelists, connects social accounts, and imports feed items into the Firework video library.

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

### 2. Authentication and Availability

All endpoints require an OAuth 2.0 Bearer token. They reuse the existing `videos:*` and `channels:*` scopes; there are no separate `ai:*` OAuth scopes.

Availability rules are independent from OAuth scopes:

* AI-curation endpoints require the business subscription's `ai_content_curation` feature. If it is disabled, the API returns `402 Payment Required`.
* Starting video-product matching consumes the business's `max_product_matching` job quota. If the quota is exhausted, the API returns `429 Too Many Requests`.
* Reading existing product matches does not require a subscription feature.

### 3. Endpoint Summary

| Endpoint                                                                | Method | Scope            | Success | Notes                                    |
| ----------------------------------------------------------------------- | ------ | ---------------- | ------- | ---------------------------------------- |
| `/api/v1/videos/{id}/product_matches`                                   | GET    | `videos:read`    | `200`   | Read job status and recommendations      |
| `/api/v1/videos/{id}/product_matches`                                   | POST   | `videos:write`   | `202`   | Start asynchronous matching              |
| `/api/v1/videos/{id}/product_matches/accept`                            | POST   | `videos:write`   | `200`   | Accept the complete desired subset       |
| `/api/v1/channels/{channel_id}/ai_curation/feed`                        | GET    | `videos:read`    | `200`   | Browse the discovery feed                |
| `/api/v1/channels/{channel_id}/ai_curation/settings`                    | GET    | `channels:read`  | `200`   | Read discovery settings                  |
| `/api/v1/channels/{channel_id}/ai_curation/settings`                    | PATCH  | `channels:write` | `200`   | Update discovery settings                |
| `/api/v1/channels/{channel_id}/ai_curation/creators`                    | GET    | `channels:read`  | `200`   | List creator-whitelist entries           |
| `/api/v1/channels/{channel_id}/ai_curation/creators`                    | POST   | `channels:write` | `201`   | Add up to 100 creators                   |
| `/api/v1/channels/{channel_id}/ai_curation/creators/{id}`               | DELETE | `channels:write` | `204`   | Remove a creator-whitelist entry         |
| `/api/v1/channels/{channel_id}/ai_curation/social_accounts`             | GET    | `channels:read`  | `200`   | List connected accounts                  |
| `/api/v1/channels/{channel_id}/ai_curation/social_accounts/connect_url` | POST   | `channels:write` | `201`   | Start a social-platform OAuth connection |
| `/api/v1/channels/{channel_id}/ai_curation/imports`                     | POST   | `videos:write`   | `202`   | Import one discovery-feed item           |

***

### 4. Video-Product Matching

#### 4.1. Workflow

1. Call `POST /api/v1/videos/{id}/product_matches` for an approved video.
2. Poll `GET /api/v1/videos/{id}/product_matches` until `status` is `completed` or `failed`.
3. If `auto_accept` was not enabled, call `POST /api/v1/videos/{id}/product_matches/accept` with the full set of matched products to keep.

Matching searches the products in the video's business store.

#### 4.2. List Product Matches

**Endpoint**: `GET /api/v1/videos/{video_id}/product_matches`\
**Scope**: `videos:read`

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

**Success Response**: `200 OK`

```json
{
  "status": "completed",
  "matches": [
    {
      "id": "MtCh42",
      "product": {
        "id": "PrOd91",
        "name": "Everyday Running Shoe",
        "external_id": "SHOE-001"
      },
      "matching_score": 93,
      "status": "pending",
      "created_at": "2026-07-20T06:10:00Z",
      "updated_at": "2026-07-20T06:10:00Z"
    }
  ]
}
```

| Field                      | Type      | Description                                                                     |
| -------------------------- | --------- | ------------------------------------------------------------------------------- |
| `status`                   | string    | Most recent job: `none`, `in_progress`, `completed`, or `failed`                |
| `matches`                  | object\[] | Recommendations, ordered by `matching_score` from best to worst                 |
| `matches[].id`             | string    | Encoded match ID                                                                |
| `matches[].product`        | object    | Product object; see the [Product API](/firework-for-developers/api/products.md) |
| `matches[].matching_score` | integer   | Higher values indicate a better match                                           |
| `matches[].status`         | string    | `pending`, `accepted`, or `rejected`                                            |
| `matches[].created_at`     | string    | ISO 8601 creation time                                                          |
| `matches[].updated_at`     | string    | ISO 8601 update time                                                            |

If matching has never been requested, the response is `{"status":"none","matches":[]}`.

#### 4.3. Start Product Matching

**Endpoint**: `POST /api/v1/videos/{video_id}/product_matches`\
**Scope**: `videos:write`\
**Content Type**: `application/json`

The request body is optional.

| Field         | Type    | Required | Default | Description                                                      |
| ------------- | ------- | -------- | ------- | ---------------------------------------------------------------- |
| `auto_accept` | boolean | ❌        | `false` | Automatically accept every returned match and attach its product |

```bash
curl -X POST "https://api.firework.com/api/v1/videos/AbC123/product_matches" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"auto_accept":false}'
```

**Success Response**: `202 Accepted`

```json
{
  "status": "in_progress",
  "auto_accept": false
}
```

The request can return `400` for malformed input, `404` when the video is not found, `422` when the video cannot be matched, and `429` when the matching quota is exhausted.

#### 4.4. Accept Product Matches

**Endpoint**: `POST /api/v1/videos/{video_id}/product_matches/accept`\
**Scope**: `videos:write`\
**Content Type**: `application/json`

`product_ids` is the complete desired set. Every current match that is not listed becomes `rejected`; listed matches become `accepted` and their products are attached to the video. Unknown IDs fail the whole request.

| Field         | Type      | Required | Description                                    |
| ------------- | --------- | -------- | ---------------------------------------------- |
| `product_ids` | string\[] | ✅        | Non-empty array of encoded matched product IDs |

```bash
curl -X POST "https://api.firework.com/api/v1/videos/AbC123/product_matches/accept" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"product_ids":["PrOd91","PrOd92"]}'
```

**Success Response**: `200 OK`, with the same `status` and `matches` shape as the list endpoint.

***

### 5. AI-Curation Discovery Feed

The discovery feed is channel-scoped. Configure settings and connect at least one social account before expecting results. Each feed item's `id` is an opaque curation-engine handle; pass it as `feed_item_id` when importing.

**Endpoint**: `GET /api/v1/channels/{channel_id}/ai_curation/feed`\
**Scope**: `videos:read`\
**Subscription feature**: `ai_content_curation`

#### 5.1. Query Parameters

| Parameter           | Type    | Required | Description                                                   |
| ------------------- | ------- | -------- | ------------------------------------------------------------- |
| `sort`              | string  | ❌        | `newest` (default) or `score`                                 |
| `source`            | string  | ❌        | `tiktok` or `instagram`                                       |
| `consent_status`    | string  | ❌        | `approved`, `pending`, `need_review`, or `denied`             |
| `whitelist_only`    | boolean | ❌        | Return only creators on the channel whitelist                 |
| `external_ids`      | string  | ❌        | Comma-separated platform IDs; requires `source`               |
| `hashtags`          | string  | ❌        | Comma-separated discovery hashtags                            |
| `language`          | string  | ❌        | Content-language filter                                       |
| `inserted_at_begin` | string  | ❌        | Include items discovered at or after this ISO 8601 timestamp  |
| `inserted_at_end`   | string  | ❌        | Include items discovered at or before this ISO 8601 timestamp |
| `after`             | string  | ❌        | Opaque forward cursor from the prior response                 |
| `page_size`         | integer | ❌        | Default 50, maximum 100                                       |

```bash
curl -X GET "https://api.firework.com/api/v1/channels/ChAn42/ai_curation/feed?source=instagram&consent_status=approved&inserted_at_begin=2026-07-01T00%3A00%3A00Z&inserted_at_end=2026-07-31T23%3A59%3A59Z&page_size=50" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"
```

#### 5.2. Feed Response

**Success Response**: `200 OK`

```json
{
  "feed_items": [
    {
      "id": "instagram:ig-18001234",
      "score": 0.94,
      "summary": "Creator demonstrates the product in natural light.",
      "quotes": ["This is how it looks outdoors"],
      "tone": "informative",
      "intent": "product_review",
      "audience": "running enthusiasts",
      "consent_status": "approved",
      "imported": false,
      "whitelisted_creator": true,
      "curated_at": "2026-07-20T05:00:00Z",
      "video": {
        "external_id": "ig-18001234",
        "source": "instagram",
        "url": "https://www.instagram.com/reel/example/",
        "caption": "Morning run test",
        "thumbnail_url": "https://cdn.example.com/thumb.jpg",
        "username": "runner_example",
        "display_name": "Runner Example",
        "duration": 24.5,
        "published_at": "2026-07-18T03:00:00Z",
        "hashtags": ["running", "shoereview"],
        "likes_count": 642,
        "views_count": 10240,
        "comments_count": 31
      }
    }
  ],
  "links": {
    "next": "/api/v1/channels/ChAn42/ai_curation/feed?after=next-cursor&page_size=50"
  },
  "pagination": { "cursor": "next-cursor", "has_more": true }
}
```

Most feed-item analysis and social metrics fields are nullable because the upstream platform may not provide them. An empty `feed_items` array usually means discovery settings are not configured or no social account is connected.

***

### 6. AI-Curation Settings

#### 6.1. Get Settings

**Endpoint**: `GET /api/v1/channels/{channel_id}/ai_curation/settings`\
**Scope**: `channels:read`

**Success Response**: `200 OK`

```json
{
  "brand_description": "Performance footwear for everyday runners",
  "tiktok_hashtags": ["running", "shoereview"],
  "instagram_hashtags": ["runclub"],
  "tiktok_mentioned_usernames": ["firework_running"],
  "instagram_mentioned_usernames": [],
  "score_threshold": 0.7,
  "last_synced_at": "2026-07-20T05:15:00Z",
  "connected_accounts": []
}
```

#### 6.2. Update Settings

**Endpoint**: `PATCH /api/v1/channels/{channel_id}/ai_curation/settings`\
**Scope**: `channels:write`\
**Content Type**: `application/json`

Any subset of the following fields is accepted. Array fields replace the stored array wholesale.

| Field                           | Type      | Required | Description                              |
| ------------------------------- | --------- | -------- | ---------------------------------------- |
| `brand_description`             | string    | ❌        | Brand context used for discovery/scoring |
| `tiktok_hashtags`               | string\[] | ❌        | TikTok discovery hashtags                |
| `instagram_hashtags`            | string\[] | ❌        | Instagram discovery hashtags             |
| `tiktok_mentioned_usernames`    | string\[] | ❌        | TikTok mentioned-account filters         |
| `instagram_mentioned_usernames` | string\[] | ❌        | Instagram mentioned-account filters      |
| `score_threshold`               | number    | ❌        | Minimum score for feed items             |

```bash
curl -X PATCH "https://api.firework.com/api/v1/channels/ChAn42/ai_curation/settings" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "brand_description":"Performance footwear for everyday runners",
    "instagram_hashtags":["runclub","shoereview"],
    "score_threshold":0.7
  }'
```

**Success Response**: `200 OK`, with the complete settings object.

***

### 7. Creator Whitelist

Whitelist entries identify creators whose posts are pre-cleared for curation. Their feed items report `whitelisted_creator: true`.

> **ID type**: Creator whitelist IDs are integers assigned by the curation engine. They are not Firework-encoded IDs.

#### 7.1. List Creators

**Endpoint**: `GET /api/v1/channels/{channel_id}/ai_curation/creators`\
**Scope**: `channels:read`

| Query parameter   | Type    | Required | Description             |
| ----------------- | ------- | -------- | ----------------------- |
| `platform`        | string  | ❌        | `tiktok` or `instagram` |
| `username`        | string  | ❌        | Exact username          |
| `include_expired` | boolean | ❌        | Include expired entries |
| `after`           | string  | ❌        | Opaque forward cursor   |
| `page_size`       | integer | ❌        | Default 50, maximum 100 |

**Success Response**: `200 OK`

```json
{
  "creators": [
    {
      "id": 481,
      "username": "runner_example",
      "creator_name": "Runner Example",
      "platform": "instagram",
      "expires_at": null,
      "created_at": "2026-07-18T08:00:00Z",
      "updated_at": "2026-07-18T08:00:00Z"
    }
  ],
  "links": { "next": null },
  "pagination": { "cursor": null, "has_more": false }
}
```

#### 7.2. Add Creators

**Endpoint**: `POST /api/v1/channels/{channel_id}/ai_curation/creators`\
**Scope**: `channels:write`\
**Content Type**: `application/json`

| Field                     | Type      | Required | Description                       |
| ------------------------- | --------- | -------- | --------------------------------- |
| `creators`                | object\[] | ✅        | 1 to 100 creator entries          |
| `creators[].username`     | string    | ✅        | Social-platform username          |
| `creators[].platform`     | string    | ✅        | `tiktok` or `instagram`           |
| `creators[].creator_name` | string    | ❌        | Display name                      |
| `creators[].expires_at`   | string    | ❌        | Optional ISO 8601 expiration time |

```bash
curl -X POST "https://api.firework.com/api/v1/channels/ChAn42/ai_curation/creators" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"creators":[{"username":"runner_example","platform":"instagram"}]}'
```

**Success Response**: `201 Created`, as `{"creators":[...]}`.

#### 7.3. Remove a Creator

**Endpoint**: `DELETE /api/v1/channels/{channel_id}/ai_curation/creators/{id}`\
**Scope**: `channels:write`

```bash
curl -X DELETE "https://api.firework.com/api/v1/channels/ChAn42/ai_curation/creators/481" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"
```

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

***

### 8. Connected Social Accounts

#### 8.1. List Accounts

**Endpoint**: `GET /api/v1/channels/{channel_id}/ai_curation/social_accounts`\
**Scope**: `channels:read`

**Success Response**: `200 OK`

```json
{
  "accounts": [
    {
      "id": 912,
      "provider": "facebook",
      "account_name": "Firework Running",
      "avatar_url": "https://cdn.example.com/avatar.jpg"
    }
  ]
}
```

Social-account `id` values are upstream integers, not Firework-encoded IDs.

#### 8.2. Create a Connection URL

**Endpoint**: `POST /api/v1/channels/{channel_id}/ai_curation/social_accounts/connect_url`\
**Scope**: `channels:write`\
**Content Type**: `application/json`

| Field               | Type   | Required | Description                                                      |
| ------------------- | ------ | -------- | ---------------------------------------------------------------- |
| `provider`          | string | ✅        | `tiktok` or `facebook`; Instagram connects through Facebook      |
| `redirect_back_uri` | string | ❌        | Browser destination after OAuth; defaults to the portal callback |

```bash
curl -X POST "https://api.firework.com/api/v1/channels/ChAn42/ai_curation/social_accounts/connect_url" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"provider":"facebook","redirect_back_uri":"https://partner.example.com/firework/callback"}'
```

**Success Response**: `201 Created`

```json
{
  "authorization_url": "https://social.example.com/oauth/authorize?...",
  "scopes": ["instagram_basic"],
  "state": "opaque-state"
}
```

Open `authorization_url` in a user-controlled browser. The social-platform OAuth flow cannot be completed solely through this API.

***

### 9. Import a Curated Feed Item

Import a feed item into the channel's Firework video library. The import is asynchronous; poll the returned job with `GET /api/v1/videos/imports/{id}` or use video webhooks.

**Endpoint**: `POST /api/v1/channels/{channel_id}/ai_curation/imports`\
**Scope**: `videos:write`\
**Subscription feature**: `ai_content_curation`\
**Content Type**: `application/json`

#### 9.1. Request Body

| Field                          | Type      | Required | Default      | Description                                       |
| ------------------------------ | --------- | -------- | ------------ | ------------------------------------------------- |
| `feed_item_id`                 | string    | ✅        | None         | Feed item `id` returned by the discovery feed     |
| `caption`                      | string    | ❌        | Feed caption | Override the imported video caption               |
| `access`                       | string    | ❌        | `public`     | `public`, `private`, or `unlisted`                |
| `hashtags`                     | string\[] | ❌        | Feed tags    | Override the imported hashtags                    |
| `poster_url`                   | string    | ❌        | None         | `.jpg`, `.jpeg`, `.png`, or `.webp` poster URL    |
| `product_matching.auto`        | boolean   | ❌        | `false`      | Run matching after the imported video is approved |
| `product_matching.auto_accept` | boolean   | ❌        | `false`      | Accept every match; requires `auto: true`         |

Matching requested here runs after video approval and consumes the matching job quota at that time; quota usage is not checked when the import request is accepted.

```bash
curl -X POST "https://api.firework.com/api/v1/channels/ChAn42/ai_curation/imports" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "feed_item_id":"instagram:ig-18001234",
    "caption":"Morning run test",
    "access":"public",
    "product_matching":{"auto":true,"auto_accept":false}
  }'
```

#### 9.2. Import Response

**Success Response**: `202 Accepted`

```json
{
  "id": "ImPo42",
  "status": "running",
  "video_id": null,
  "created_at": "2026-07-20T06:30:00Z",
  "completed_at": null
}
```

Re-importing the same feed item into the same channel returns `422 Unprocessable Entity`. A feed item without downloadable media also returns `422`.

***

### 10. Common Error Responses

| Status Code                | Description                                                   |
| -------------------------- | ------------------------------------------------------------- |
| `400 Bad Request`          | Malformed body, invalid query combination, or invalid cursor  |
| `401 Unauthorized`         | Missing, invalid, or expired OAuth token                      |
| `402 Payment Required`     | `ai_content_curation` is not enabled for the business         |
| `403 Forbidden`            | Missing scope or no access to the video/channel               |
| `404 Not Found`            | Video, channel, creator entry, or upstream resource not found |
| `422 Unprocessable Entity` | Valid JSON that violates the endpoint's domain rules          |
| `429 Too Many Requests`    | Product-matching job quota or request rate limit exhausted    |
| `502 Bad Gateway`          | The upstream AI-curation service failed or timed out          |

Errors use the standard shape:

```json
{
  "error": "Error Message"
}
```
