Source: https://developers.tokportal.com/bundles/
Markdown: https://developers.tokportal.com/bundles.md

# Bundles

A **bundle** groups an account configuration and optional video configuration for a specific country and platform. Bundles are the core unit of work in the TokPortal API. **One bundle is exactly one account** — to order several accounts use [`POST /bundles/bulk`](https://developers.tokportal.com/bundles/create-bulk), and to post across several accounts create one bundle per account.

## Authentication

All endpoints require an API key passed via the `X-API-Key` header.

```
X-API-Key: sk_xxx
```

## Base URL

```
https://app.tokportal.com/api/ext
```

## Bundle Types

| Type                 | Description                                 |
| -------------------- | ------------------------------------------- |
| `account_only`       | Account creation and setup only. No videos. |
| `account_and_videos` | Account creation with video uploads.        |
| `videos_only`        | Video uploads on an existing account.       |

## Lifecycle

Bundles follow a strict lifecycle:

```
pending_setup → published → accepted → completed
```

For bundles that include videos, the video phase adds intermediate states:

```
accepted → in_review → finalized → completed
```

| Status          | Description                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  |
| --------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `pending_setup` | Bundle created, awaiting configuration.                                                                                                                                                                                                                                                                                                                                                                                                                                                                      |
| `published`     | Configuration complete, submitted for processing.                                                                                                                                                                                                                                                                                                                                                                                                                                                            |
| `accepted`      | Account work has been accepted by an operator.                                                                                                                                                                                                                                                                                                                                                                                                                                                               |
| `in_review`     | Videos are being reviewed.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                   |
| `finalized`     | Videos have been finalized.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  |
| `completed`     | All work is done.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                            |
| `cancelled`     | Terminal. The bundle will never accept publishes again — most commonly because the account was banned, or after an accepted ban-check with the cancel & refund resolution. `cancelled_at` and `cancelled_reason` say when and why; a [`bundle.cancelled` webhook](https://developers.tokportal.com/webhooks#the-bundlecancelled-event) fires when it happens. Publish calls return `409 BUNDLE_INVALID_STATUS` with `current_status: "cancelled"` plus the same `cancelled_at` / `cancelled_reason` details. |

> **INFO: Automatic finalization after review**
> Account listings and video listings that remain in `in_review` without user approval or a correction request are automatically moved to `finalized` after **72 hours**. The window starts when the individual listing enters `in_review`, not when the bundle was created or published.
>
> This applies **regardless of `auto_finalize_videos`** — that flag decides whether delivered videos are approved immediately, not whether the review window eventually expires. Setting it to `false` gives you the window; it does not hold work in review indefinitely. Act inside the window with `POST /bundles/:id/videos/:position/finalize` or a corrections request.

## Platforms

| Platform  | Value       |
| --------- | ----------- |
| TikTok    | `tiktok`    |
| Instagram | `instagram` |

> **INFO: YouTube is not orderable through the API**
> Bundle creation supports **TikTok and Instagram only**. A YouTube bundle is rejected with `400 YOUTUBE_DELAYED`, even though `GET /credit-costs` lists a `youtube_account_creation` price — the price is real, the order path is not open. YouTube accounts are delivered through a separate, non-API flow; contact the team. YouTube accounts are outside TokPortal Coverage.

## Options

| Option                         | Description                                                                                                                                                                                                          | Restrictions                                                                                                                                                                                                                                                                                                                                                                                                           |
| ------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `wants_advanced_warming`       | Enable **Advanced Niche Warming**: niche-target (search-term) based warming with recorded proof and a data report. 5 credits per target — per target, never per day. `GET /credit-costs` returns the effective rate. | TikTok/Instagram only (`ADVANCED_WARMING_PLATFORM`). **Mandatory whenever `advanced_warming_terms` or `advanced_warming_terms_count` is sent** — TokPortal never infers it, and targets sent without it are refused with `ADVANCED_WARMING_FLAG_REQUIRED` rather than charged and dropped. Requires one of those two fields (3-30, multiples of 3). Cannot combine with the legacy warming flags (`WARMING_CONFLICT`). |
| `advanced_warming_terms`       | The niche targets for Advanced Niche Warming.                                                                                                                                                                        | 3-30 terms (count must be a multiple of 3), each 2-50 characters, all distinct. Requires `wants_advanced_warming: true`. Terms are trimmed and compared case-insensitively; if any is dropped as a duplicate or for its length the call fails with `ADVANCED_WARMING_TERMS_REJECTED` instead of silently shrinking the order.                                                                                          |
| `advanced_warming_terms_count` | Buy a target count now and configure the targets later.                                                                                                                                                              | 3-30, multiple of 3. Requires `wants_advanced_warming: true`. Do not send it alongside `advanced_warming_terms` unless it equals that list's length — a disagreement fails with `ADVANCED_WARMING_COUNT_MISMATCH`, neither quantity silently wins. Configure once with `PUT /bundles/:id/warming-terms`; unconfigured purchases are refunded after 14 days.                                                            |
| `wants_niche_warming`          | **⚠️ Deprecated.** Legacy — kept only for accounts being re-created after a refund; use Advanced Niche Warming (`wants_advanced_warming`).                                                                           | —                                                                                                                                                                                                                                                                                                                                                                                                                      |
| `wants_deep_warming`           | **⚠️ Deprecated.** Legacy — kept only for accounts being re-created after a refund; use Advanced Niche Warming (`wants_advanced_warming`).                                                                           | —                                                                                                                                                                                                                                                                                                                                                                                                                      |
| `wants_moderation`             | **⚠️ Deprecated — ignored.** Replaced by the [Comments API](https://developers.tokportal.com/comments-overview).                                                                                                     | —                                                                                                                                                                                                                                                                                                                                                                                                                      |
| `edits_quantity`               | Add edit slots.                                                                                                                                                                                                      | Cannot exceed `videos_quantity`.                                                                                                                                                                                                                                                                                                                                                                                       |
| `auto_finalize_videos`         | Approve each video as soon as it is delivered. Defaults to `true` — set it to `false` to review deliveries yourself.                                                                                                 | Either value leaves the 72-hour automatic finalization described above in place.                                                                                                                                                                                                                                                                                                                                       |

## Endpoints

> **INFO: TokPortal Coverage belongs to the account**
> The Coverage boundary is the authoritative timestamp recorded by the production migration. Every TikTok or Instagram saved account that already exists at that cutover remains permanently grandfathered. A saved account created after it receives one TokPortal Coverage record when its account listing reaches `in_review`, unless its creation response marks it contractually exempt. Coverage is keyed by `saved_account_id`, not `bundle_id`. Bulk creation, single creation, dashboard creation, API creation and either MCP transport all converge on the same rule. Additional `videos_only` bundles on the same account never add another record or another 25-credit charge. The first 30 days are included after review submission, and the first 25-credit debit on day 30 covers the next 30 days. Bundle completion or cancellation never changes Coverage.

Bundles with no newly scheduled or posted video for 60 days are archived from the manager's active workspace, with `inactive_archived_at` and `inactive_archive_reason` returned by the API. This is not a cancellation, does not refund anything, and does not change TokPortal Coverage. New work reopens the bundle automatically.

| Method  | Path                             | Description                                                                                                  |
| ------- | -------------------------------- | ------------------------------------------------------------------------------------------------------------ |
| `POST`  | `/bundles`                       | [Create a bundle](https://developers.tokportal.com/bundles/create-bundle)                                    |
| `GET`   | `/bundles`                       | List all bundles                                                                                             |
| `GET`   | `/bundles/:id`                   | Get a single bundle                                                                                          |
| `PATCH` | `/bundles/:id`                   | Update bundle settings                                                                                       |
| `GET`   | `/bundles/:id/publish-readiness` | [Check publish-readiness](https://developers.tokportal.com/bundles/#check-publish-readiness)                 |
| `POST`  | `/bundles/:id/publish`           | [Publish a bundle](https://developers.tokportal.com/bundles/publish-unpublish)                               |
| `POST`  | `/bundles/:id/unpublish`         | [Unpublish a bundle](https://developers.tokportal.com/bundles/publish-unpublish)                             |
| `PUT`   | `/bundles/:id/warming-terms`     | Configure a deferred [Advanced Niche Warming](https://developers.tokportal.com/advanced-warming) target list |

## Computed State Fields

Both `GET /bundles` and `GET /bundles/:id` return a set of **read-only computed fields** to make integration logic simpler. These derive from the underlying bundle / account / video state — you do not need to recompute them client-side.

| Field                     | Type           | Description                                                                                                                                                             |
| ------------------------- | -------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `is_published`            | boolean        | `true` once the bundle has been published (status is `published`, `published_priority`, `accepted`, or `completed`).                                                    |
| `account_configured`      | boolean        | `true` when the bundle's account is in `configured` or `finalized` status (i.e., publish-eligible).                                                                     |
| `videos_total`            | integer        | Number of video slots on the bundle.                                                                                                                                    |
| `videos_configured_count` | integer        | Number of video slots whose status indicates they have been configured (any of `configured`, `in_review`, `pending_corrections`, `accepted`, `published`, `finalized`). |
| `next_action`             | string \| null | Suggested next step in the configure → publish flow. One of `configure_account`, `configure_videos`, `publish_bundle`, or `null` if no action is required.              |

`next_action` is the recommended primary signal for an integration:

- `configure_account` — call `PUT /bundles/:id/account` to set the account fields.
- `configure_videos` — call `PUT /bundles/:id/videos/batch` (or per-position) to fill the video slots.
- `publish_bundle` — call `POST /bundles/:id/publish` (or pass `auto_publish: true` on the next video configuration call).
- `null` — the bundle is already past setup; nothing to do.

### Remake info

Both `GET /bundles` and `GET /bundles/:id` return a `remade` object. When an account is banned or lost, TokPortal **remakes** it: the bundle is rebuilt **in place under the same `bundle_id`** and only the account handle changes. Because the `bundle_id` and your `external_ref` are stable across remakes, they are the recommended anchors for tracking an account that may be replaced over time.

| Field          | Type                      | Description                                                                                               |
| -------------- | ------------------------- | --------------------------------------------------------------------------------------------------------- |
| `was_remade`   | boolean                   | `true` if this bundle was remade at least once, otherwise `false`.                                        |
| `remade_count` | integer                   | Number of times the bundle has been remade (`0` when never remade).                                       |
| `old_username` | string \| null            | The previous username, captured at the time of the most recent remake. `null` if unknown or never remade. |
| `remade_at`    | string (ISO 8601) \| null | Timestamp of the most recent remake. `null` if never remade.                                              |

> **TIP: tip**
> Subscribe to the [`account.remade` webhook](https://developers.tokportal.com/webhooks#the-accountremade-event) to be notified of remakes in real time instead of polling.

### Cancellation info

Both `GET /bundles` and `GET /bundles/:id` return `cancelled_at` and `cancelled_reason` (both `null` unless the bundle is `cancelled`). When a validated ban cancels the bundle, `cancelled_reason` starts with `Account banned:` and the [`account.banned`](https://developers.tokportal.com/webhooks#the-accountbanned-event) + [`bundle.cancelled`](https://developers.tokportal.com/webhooks#the-bundlecancelled-event) webhooks fire; the banned account itself stays readable on `GET /accounts/{id}` with its `ban` details.

***

## List Bundles

Retrieve all bundles for your organization.

```
GET /bundles
```

### Query Parameters

| Param            | Type    | Description                                                                                                                                      |
| ---------------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------ |
| `status`         | string  | Filter by bundle status: `pending_setup`, `published`, `accepted`, `completed`, etc.                                                             |
| `bundle_type`    | string  | Filter by type: `account_only`, `account_and_videos`, `videos_only`.                                                                             |
| `account_status` | string  | Filter by account listing status: `pending`, `configured`, `in_review`, `finalized`. Useful for finding bundles with accounts ready to finalize. |
| `platform`       | string  | Filter by platform: `tiktok`, `instagram`, `youtube`.                                                                                            |
| `external_ref`   | string  | Filter by your external reference.                                                                                                               |
| `page`           | integer | Page number (default: 1).                                                                                                                        |
| `per_page`       | integer | Results per page (default: 25, max: 100).                                                                                                        |

### Example

```bash
# List all bundles with accounts awaiting finalization
curl -X GET "https://app.tokportal.com/api/ext/bundles?account_status=in_review" \
  -H "X-API-Key: sk_xxx"
```

### Response

```json
{
  "data": [
    {
      "id": "bnd_abc123",
      "bundle_type": "account_and_videos",
      "platform": "tiktok",
      "country": "US",
      "status": "accepted",
      "account_status": "in_review",
      "videos_quantity": 5,
      "edits_quantity": 0,
      "used_edits": 0,
      "credit_cost": 60,
      "external_ref": "campaign-42",
      "existing_account_id": null,
      "auto_finalize_videos": false,
      "wants_advanced_warming": false,
      "advanced_warming_terms": [],
      "wants_moderation": false,
      "is_published": true,
      "account_configured": true,
      "videos_total": 5,
      "videos_configured_count": 5,
      "next_action": null,
      "remade": {
        "was_remade": false,
        "remade_count": 0,
        "old_username": null,
        "remade_at": null
      },
      "created_at": "2026-01-15T10:30:00Z",
      "updated_at": "2026-01-15T12:00:00Z"
    }
  ]
}
```

***

## Get Bundle

Retrieve a single bundle by ID.

```
GET /bundles/:id
```

### Example

```bash
curl -X GET https://app.tokportal.com/api/ext/bundles/bnd_abc123 \
  -H "X-API-Key: sk_xxx"
```

### Response

```json
{
  "data": {
    "id": "bnd_abc123",
    "bundle_type": "account_and_videos",
    "platform": "tiktok",
    "country": "US",
    "status": "published",
    "videos_quantity": 3,
    "edits_quantity": 0,
    "used_edits": 0,
    "credit_cost": 40,
    "external_ref": "campaign-42",
    "existing_account_id": null,
    "auto_finalize_videos": false,
    "wants_advanced_warming": false,
    "advanced_warming_terms": [],
    "wants_moderation": false,
    "is_published": true,
    "account_configured": true,
    "videos_total": 3,
    "videos_configured_count": 3,
    "next_action": null,
    "remade": {
      "was_remade": true,
      "remade_count": 1,
      "old_username": "cooluser123",
      "remade_at": "2026-02-02T09:15:00Z"
    },
    "saved_account_id": "7c9e0f1a-2b3c-4d5e-8f6a-1b2c3d4e5f60",
    "account": {
      "id": "al_abc123",
      "saved_account_id": "7c9e0f1a-2b3c-4d5e-8f6a-1b2c3d4e5f60",
      "username": "cooluser123",
      "visible_name": "Cool User",
      "biography": "Just vibes.",
      "profile_picture_url": "https://example.com/pic.jpg"
    },
    "videos": [
      {
        "id": "vid_xyz",
        "position": 1,
        "status": "pending",
        "name": "Video 1"
      }
    ],
    "created_at": "2026-01-15T10:30:00Z",
    "updated_at": "2026-01-15T12:00:00Z"
  }
}
```

> **NOTE: `saved_account_id` vs `account.id`**
> `GET /bundles/:id` returns `saved_account_id` (top-level and inside `account`) — the id of the **real created account**, i.e. what `GET /accounts/{id}` uses. It is `null` until the account exists, and changes after a remake. The `account.id` field is the **account listing** id (the bundle's account slot), which is stable across remakes. Use `saved_account_id` whenever you need to act on the actual account (reveal credentials, fetch the latest handle, etc.). This is the clean way to obtain the new account id after a remake — poll this endpoint once you see `account.finalized`, or read it straight from the `account.in_review` / `account.finalized` webhooks.

***

## Update Bundle

Update settings on an existing bundle. You can modify `auto_finalize_videos`, `external_ref`, and `title` at any time, regardless of bundle status.

```
PATCH /bundles/:id
```

### Request Body

All fields are optional. At least one must be provided.

| Field                  | Type           | Description                                                                                                                                                                                                                     |
| ---------------------- | -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `auto_finalize_videos` | boolean        | Toggle auto-approval of videos. `true` (the default on creation) approves each delivery on arrival; `false` keeps the review window. Neither value disables the 72-hour automatic finalization of anything left in `in_review`. |
| `external_ref`         | string \| null | Your own reference ID. Max 200 characters. Must be unique per user. Set to `null` to clear.                                                                                                                                     |
| `title`                | string \| null | Bundle title. Max 200 characters. Set to `null` to clear.                                                                                                                                                                       |

### Example

```bash
curl -X PATCH https://app.tokportal.com/api/ext/bundles/bnd_abc123 \
  -H "X-API-Key: sk_xxx" \
  -H "Content-Type: application/json" \
  -d '{
    "auto_finalize_videos": false,
    "external_ref": "campaign-42-updated"
  }'
```

### Response

```json
{
  "data": {
    "id": "bnd_abc123",
    "title": "My Campaign",
    "auto_finalize_videos": false,
    "external_ref": "campaign-42-updated",
    "updated_at": "2026-02-10T14:30:00Z"
  }
}
```

### Error Responses

| Status | Code                     | Description                                      |
| ------ | ------------------------ | ------------------------------------------------ |
| `400`  | `VALIDATION_ERROR`       | No fields provided, or invalid values.           |
| `404`  | `BUNDLE_NOT_FOUND`       | Bundle does not exist.                           |
| `403`  | `BUNDLE_NOT_OWNED`       | Bundle belongs to another user.                  |
| `409`  | `DUPLICATE_EXTERNAL_REF` | Another bundle already uses this `external_ref`. |

***

## Check Publish-Readiness

Returns whether a bundle is currently ready for `POST /bundles/:id/publish`, and if not, the list of blockers that publish would raise. **Read-only — no side effects, no credit charge.** Useful to surface "what's missing" in your UI before attempting publish.

```
GET /bundles/:id/publish-readiness
```

### Example

```bash
curl -X GET https://app.tokportal.com/api/ext/bundles/bnd_abc123/publish-readiness \
  -H "X-API-Key: sk_xxx"
```

### Response — ready

```json
{
  "data": {
    "bundle_id": "bnd_abc123",
    "ready": true,
    "blockers": []
  }
}
```

### Response — blockers present

```json
{
  "data": {
    "bundle_id": "bnd_abc123",
    "ready": false,
    "blockers": [
      {
        "code": "ACCOUNT_MISSING_FIELDS",
        "message": "Account is missing required fields.",
        "details": { "missing_fields": ["profile_picture_url"] }
      }
    ]
  }
}
```

### Blocker Codes

| Code                       | Meaning                                                                                                 |
| -------------------------- | ------------------------------------------------------------------------------------------------------- |
| `BUNDLE_ALREADY_PUBLISHED` | The bundle is already published or accepted.                                                            |
| `BUNDLE_NOT_IN_SETUP`      | The bundle's status is not `pending_setup` (e.g., `draft`, `cancelled`).                                |
| `ACCOUNT_MISSING`          | No account configuration exists yet for the bundle.                                                     |
| `ACCOUNT_NOT_CONFIGURED`   | The account exists but its status is not yet `configured` or `finalized`.                               |
| `ACCOUNT_MISSING_FIELDS`   | The account is missing one or more required fields (`username`, `visible_name`, `profile_picture_url`). |
