Source: https://developers.tokportal.com/create-bundle/
Markdown: https://developers.tokportal.com/create-bundle.md

# Create Bundle

Create a new bundle to provision an account, upload videos, or both.

```
POST /bundles
```

This request is the paid creation checkout. On success, TokPortal creates the bundle and debits the server-calculated credits immediately in one transaction. Publication happens later and never repeats the setup charge or consumes another contractual allowance slot.

All fields are **top-level** in the request body. There is no `options` wrapper.

## Request Body

### Required

| Field         | Type   | Description                                                   |
| ------------- | ------ | ------------------------------------------------------------- |
| `bundle_type` | string | `"account_only"`, `"account_and_videos"`, or `"videos_only"`. |

### Conditional

| Field             | Type          | Required When                        | Description                                                                                                                                                                                                                                                                                                                                                                                                     |
| ----------------- | ------------- | ------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `country`         | string        | `account_only`, `account_and_videos` | TokPortal country code or supported alias. Use `GET /countries` to list countries currently available for new account creation. Disabled countries and countries restricted to existing-account `videos_only` work are excluded. Optional for `videos_only` (derived from the saved account).                                                                                                                   |
| `account_id`      | string (UUID) | `videos_only`                        | The single saved account this bundle posts to. **One bundle targets exactly one account** — to post across several accounts, create one bundle per account. **Only valid with `bundle_type: "videos_only"`** — passing it on `account_and_videos` or `account_only` returns `ACCOUNT_ID_NOT_ALLOWED` (this prevents accidentally being charged the account-creation credits while reusing an existing account). |
| `videos_quantity` | integer       | `account_and_videos`, `videos_only`  | Number of video slots. Must be greater than `0`.                                                                                                                                                                                                                                                                                                                                                                |

### Optional

| Field                          | Type      | Default    | Description                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                     |
| ------------------------------ | --------- | ---------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `platform`                     | string    | `"tiktok"` | `"tiktok"` or `"instagram"` — those are the only platforms that can be ordered. `"youtube"` is rejected with `YOUTUBE_DELAYED` even though `GET /credit-costs` lists a `youtube_account_creation` price; the price is real, the order path is not open.                                                                                                                                                                                                                                                                                                                                                                                         |
| `title`                        | string    | —          | Bundle title. Max 200 characters.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                               |
| `edits_quantity`               | integer   | `0`        | Number of video edits. Cannot exceed `videos_quantity`.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                         |
| `wants_advanced_warming`       | boolean   | `false`    | Enable **Advanced Niche Warming**, the warming product: niche-target (search-term) based warming with recorded proof and a data report. The standard rate is 5 credits per target — per target, never per day — with a 15-credit minimum. A workspace-specific `contract_bundle_allowance` can return another effective rate; `GET /credit-costs` is the authority. TikTok and Instagram only. Requires `advanced_warming_terms_count` **or** `advanced_warming_terms`, and is **mandatory whenever either of them is sent** — it is never inferred. See the [Advanced Niche Warming guide](https://developers.tokportal.com/advanced-warming). |
| `advanced_warming_terms_count` | integer   | —          | Number of niche targets to purchase: 3-30, multiples of 3. Requires `wants_advanced_warming: true` in the same body. **Recommended flow:** buy the count now and write the actual targets later via [`PUT /bundles/{id}/warming-terms`](https://developers.tokportal.com/advanced-warming). Do not send it alongside `advanced_warming_terms` unless it equals that list's length. Purchases never configured within 14 days are auto-cancelled and fully refunded.                                                                                                                                                                             |
| `advanced_warming_terms`       | string\[] | —          | Alternative to `advanced_warming_terms_count`: provide the niche targets immediately (3-30 terms, count multiple of 3, each 2-50 characters, all distinct). Requires `wants_advanced_warming: true` in the same body.                                                                                                                                                                                                                                                                                                                                                                                                                           |
| `wants_niche_warming`          | boolean   | `false`    | **⚠️ Deprecated.** Legacy — kept only for accounts being re-created after a refund; use Advanced Niche Warming (`wants_advanced_warming`).                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                      |
| `wants_deep_warming`           | boolean   | `false`    | **⚠️ Deprecated.** Legacy — kept only for accounts being re-created after a refund; use Advanced Niche Warming (`wants_advanced_warming`).                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                      |
| `wants_moderation`             | boolean   | `false`    | **⚠️ Deprecated — no longer applied.** Was the per-bundle "community management" add-on (25 credits). Use the [Comments API](https://developers.tokportal.com/comments-overview) instead. Sending it has no effect.                                                                                                                                                                                                                                                                                                                                                                                                                             |
| `niche_warming_instructions`   | string    | —          | **⚠️ Deprecated.** Legacy — kept only for accounts being re-created after a refund; use Advanced Niche Warming (`wants_advanced_warming`).                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                      |
| `moderation_notice`            | string    | —          | **⚠️ Deprecated — no longer applied.** Was the guidelines notice for community management. Use the [Comments API](https://developers.tokportal.com/comments-overview) instead.                                                                                                                                                                                                                                                                                                                                                                                                                                                                  |
| `auto_finalize_videos`         | boolean   | `true`     | Delivered videos are approved automatically and count as final. Set it to `false` to review each delivery yourself — approve it or request corrections — before it is finalized. Either way, this does **not** disable the automatic finalization that happens about **72 hours** after a video enters `in_review`. Changeable at any time via [Update Bundle](https://developers.tokportal.com/bundles#update-bundle).                                                                                                                                                                                                                         |
| `external_ref`                 | string    | -          | Optional client correlation reference and duplicate-detection aid for new-account bundles. Max 200 characters. Reusing it can return `DUPLICATE_ACCOUNT_BUNDLE`; it does not provide retry/replay.                                                                                                                                                                                                                                                                                                                                                                                                                                              |

> **WARNING: Community management is deprecated**
> The per-bundle "community management" / content-moderation add-on (`wants_moderation`, 25 credits) has been **replaced by the standalone [Comments API](https://developers.tokportal.com/comments-overview)**, which lets you post comments and reply to specific comments from your managed accounts. `wants_moderation` / `moderation_notice` are still accepted for backward compatibility but are **ignored** (no credits charged, no moderation provisioned).
>
> TokPortal Coverage is different from this deprecated option. The creation checkout immediately charges the standard 32-credit account setup price or the exact price returned by an active `contract_bundle_allowance`. The allowance slot is consumed atomically with that debit; creating a draft or publishing later does not consume or charge again. The production migration records the Coverage cutoff. Every TikTok or Instagram saved account that already existed at that instant remains grandfathered; a saved account created afterwards is Coverage-eligible unless the creation response marks it contractually exempt. For a later eligible account, Coverage starts when the saved account exists and is submitted for client review. The first 30 days are included, then the first 25-credit debit occurs on day 30 and covers the following 30 days. Coverage is attached once to the resulting `saved_account_id`; reusing that account in later bundles never creates another record. A bundle in `pending_setup` or `published` cannot activate Coverage. Once activated, bundle completion, cancellation, or inactivity does not stop it.

## Validation Rules

- `platform` must be `tiktok` or `instagram`. `youtube` is rejected with `YOUTUBE_DELAYED`.
- `wants_advanced_warming: true` is **mandatory** whenever `advanced_warming_terms` or `advanced_warming_terms_count` is present. TokPortal never infers it: targets sent without the flag (absent **or** explicitly `false`) are refused with `ADVANCED_WARMING_FLAG_REQUIRED` and nothing is charged.
- `wants_advanced_warming` requires **either** `advanced_warming_terms_count` **or** `advanced_warming_terms` (3-30 targets, multiples of 3), otherwise `ADVANCED_WARMING_TERMS`. The standard minimum warming charge is 15 credits; use the active `contract_bundle_allowance` when present. TikTok and Instagram only (`ADVANCED_WARMING_PLATFORM`).
- Every term in `advanced_warming_terms` must survive cleanup intact. Entries are trimmed and compared case-insensitively; if any is dropped as a duplicate or for being outside 2-50 characters, the call fails with `ADVANCED_WARMING_TERMS_REJECTED` and `details.dropped_terms`. TokPortal will not deliver — or bill — fewer targets than you asked for, so the request is refused rather than silently shrunk.
- If you send both `advanced_warming_terms` and `advanced_warming_terms_count`, they must agree. A disagreement fails with `ADVANCED_WARMING_COUNT_MISMATCH`; neither quantity silently wins.
- `edits_quantity` cannot exceed `videos_quantity`.
- `country` must be available for new account creation. Disabled countries and countries restricted to existing-account `videos_only` work are rejected. `US` and `GB` aliases are accepted and normalized by the API.
- An unrecognised field in the body is rejected with `UNKNOWN_FIELD`. This is a full-replace write, so a misspelled name is refused rather than dropped — see [the note on the account configuration page](https://developers.tokportal.com/account-configuration#unrecognised-fields-are-refused-not-dropped).

> **NOTE: Legacy warming flags**
> `wants_niche_warming`, `wants_deep_warming` and `niche_warming_instructions` are kept only for accounts being re-created after a refund. Their behaviour is unchanged: `wants_niche_warming` and `wants_advanced_warming` remain mutually exclusive (`WARMING_CONFLICT`), `niche_warming_instructions` is still required when `wants_niche_warming` is `true`, and `wants_deep_warming: true` on a new order is still rejected with `DEEP_WARMING_DEPRECATED`. New integrations should use Advanced Niche Warming.

`GET /countries` keeps new-account countries in `data` and exposes countries available for existing-account video orders in `videos_only_countries`. For example, a country can be absent from `data` but present in `videos_only_countries`; in that case, new account creation is blocked while a `videos_only` order linked to an existing account remains supported.

## Retry safety and `external_ref`

Use `Idempotency-Key` for every create call you may retry. It is the only field that claims one exact method, path, and body and replays a completed response without creating or debiting again.

For `account_only` and `account_and_videos`, `external_ref` is useful for your own correlation and for duplicate detection. Reusing a value already attached to one of your bundles returns `409 DUPLICATE_ACCOUNT_BUNDLE` with `details.existing_bundle_id`; it does not replay the earlier response. Without an `external_ref`, TokPortal also applies a short same-platform duplicate guard. Neither guard replaces `Idempotency-Key` when a response is lost or uncertain.

## Response

```json
{
  "data": {
    "bundle_id": "...",
    "bundle_type": "account_and_videos",
    "platform": "tiktok",
    "country": "US",
    "status": "pending_setup",
    "videos_quantity": 5,
    "edits_quantity": 2,
    "external_ref": "my-campaign-001",
    "review_mode": "auto",
    "review_mode_notice": "auto_finalize_videos defaulted to true: delivered videos are approved automatically and count as final. Send auto_finalize_videos: false (or PATCH the bundle) to review each video and request corrections before it is finalized.",
    "created_at": "2026-08-15T09:00:00Z"
  },
  "credits_charged": 48,
  "credits_remaining": 452,
  "cost_breakdown": {
    "account_creation": 32,
    "video_slots": 10,
    "edit_slots": 6,
    "advanced_warming": 0,
    "niche_warming": 0,
    "deep_warming": 0,
    "moderation": 0,
    "total": 48
  }
}
```

| Field                     | Type    | Description                                                                                                                                                                                                                                      |
| ------------------------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `data.bundle_id`          | string  | Unique identifier for the created bundle.                                                                                                                                                                                                        |
| `data.bundle_type`        | string  | The bundle type as submitted.                                                                                                                                                                                                                    |
| `data.platform`           | string  | Target platform.                                                                                                                                                                                                                                 |
| `data.country`            | string  | Country code.                                                                                                                                                                                                                                    |
| `data.status`             | string  | Initial bundle status.                                                                                                                                                                                                                           |
| `data.videos_quantity`    | integer | Number of video slots (if applicable).                                                                                                                                                                                                           |
| `data.edits_quantity`     | integer | Number of edits (if applicable).                                                                                                                                                                                                                 |
| `data.external_ref`       | string  | Your external reference (if provided).                                                                                                                                                                                                           |
| `data.review_mode`        | string  | `auto` or `manual` — how delivered videos are settled for this bundle. `auto` (the default) approves each delivery on arrival; `manual` holds it for your review.                                                                                |
| `data.review_mode_notice` | string  | Present **only when you omitted `auto_finalize_videos`**, to say the default was applied and how to opt into manual review. It disappears as soon as you send the field, so its presence means "you did not choose", not "something went wrong". |
| `data.created_at`         | string  | ISO 8601 timestamp.                                                                                                                                                                                                                              |
| `credits_charged`         | integer | Credits deducted for this bundle.                                                                                                                                                                                                                |
| `credits_remaining`       | integer | Your remaining credit balance.                                                                                                                                                                                                                   |
| `cost_breakdown`          | object  | Itemized cost breakdown.                                                                                                                                                                                                                         |

***

## Examples

### 1. Account and Videos (with Advanced Niche Warming)

```bash
curl -X POST https://app.tokportal.com/api/ext/bundles \
  -H "X-API-Key: sk_xxx" \
  -H "Idempotency-Key: create-campaign-42-v1" \
  -H "Content-Type: application/json" \
  -d '{
    "bundle_type": "account_and_videos",
    "platform": "tiktok",
    "country": "US",
    "videos_quantity": 5,
    "edits_quantity": 2,
    "wants_advanced_warming": true,
    "advanced_warming_terms": [
      "home gym setup", "gym motivation", "beginner workout plan",
      "push day routine", "protein shake recipes", "fitness transformation"
    ],
    "external_ref": "campaign-42"
  }'
```

> `wants_advanced_warming: true` is not optional here. Sending `advanced_warming_terms` on its own is refused with `ADVANCED_WARMING_FLAG_REQUIRED` — the flag is what is priced and what creates the warming session.

**Response:**

```json
{
  "data": {
    "bundle_id": "bnd_a1b2c3d4",
    "bundle_type": "account_and_videos",
    "platform": "tiktok",
    "country": "US",
    "status": "pending_setup",
    "videos_quantity": 5,
    "edits_quantity": 2,
    "external_ref": "campaign-42",
    "created_at": "2026-08-15T09:00:00Z"
  },
  "credits_charged": 78,
  "credits_remaining": 422,
  "cost_breakdown": {
    "account_creation": 32,
    "video_slots": 10,
    "edit_slots": 6,
    "advanced_warming": 30,
    "niche_warming": 0,
    "deep_warming": 0,
    "moderation": 0,
    "total": 78
  }
}
```

### 2. Account Only (with Advanced Niche Warming, targets configured later)

```bash
curl -X POST https://app.tokportal.com/api/ext/bundles \
  -H "X-API-Key: sk_xxx" \
  -H "Idempotency-Key: create-fr-lifestyle-v1" \
  -H "Content-Type: application/json" \
  -d '{
    "bundle_type": "account_only",
    "platform": "instagram",
    "country": "FR",
    "wants_advanced_warming": true,
    "advanced_warming_terms_count": 9,
    "title": "French lifestyle account"
  }'
```

**Response:**

```json
{
  "data": {
    "bundle_id": "bnd_e5f6g7h8",
    "bundle_type": "account_only",
    "platform": "instagram",
    "country": "FR",
    "status": "pending_setup",
    "videos_quantity": 0,
    "edits_quantity": 0,
    "wants_advanced_warming": true,
    "advanced_warming_terms": [],
    "advanced_warming_terms_count": 9,
    "external_ref": null,
    "created_at": "2026-08-15T09:05:00Z"
  },
  "credits_charged": 77,
  "credits_remaining": 423,
  "cost_breakdown": {
    "account_creation": 32,
    "video_slots": 0,
    "edit_slots": 0,
    "advanced_warming": 45,
    "niche_warming": 0,
    "deep_warming": 0,
    "moderation": 0,
    "total": 77
  }
}
```

> The 9 niche targets are written afterwards on the bundle: `PUT /bundles/{id}/warming-terms` with `advanced_warming_terms`. See the [Advanced Niche Warming guide](https://developers.tokportal.com/advanced-warming).

### 3. Videos Only (existing account)

> **Recommendation:** Instead of creating a `videos_only` bundle, consider using the [add-video-slots](https://developers.tokportal.com/publish-unpublish) endpoint on an existing active bundle. This preserves the same account manager, which is better for continuity and turnaround time.

```bash
curl -X POST https://app.tokportal.com/api/ext/bundles \
  -H "X-API-Key: sk_xxx" \
  -H "Idempotency-Key: videos-restock-wave-2-v1" \
  -H "Content-Type: application/json" \
  -d '{
    "bundle_type": "videos_only",
    "account_id": "9f3a7b2e-1c4d-4e8f-a5b6-7d9e0f1a2b3c",
    "videos_quantity": 3,
    "external_ref": "restock-wave-2"
  }'
```

**Response:**

```json
{
  "data": {
    "bundle_id": "bnd_i9j0k1l2",
    "bundle_type": "videos_only",
    "platform": "tiktok",
    "country": "US",
    "status": "pending_setup",
    "videos_quantity": 3,
    "edits_quantity": 0,
    "external_ref": "restock-wave-2",
    "created_at": "2026-02-10T09:10:00Z"
  },
  "credits_charged": 6,
  "credits_remaining": 381,
  "cost_breakdown": {
    "account_creation": 0,
    "video_slots": 6,
    "edit_slots": 0,
    "advanced_warming": 0,
    "niche_warming": 0,
    "deep_warming": 0,
    "moderation": 0,
    "total": 6
  }
}
```

> Note: `platform` and `country` are derived from the saved account when using `videos_only`.

## Error Responses

| Status | Code                              | Description                                                                                                                                                                                                                                                                |
| ------ | --------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `400`  | `VALIDATION_ERROR`                | Invalid or missing fields (see `details` for specifics).                                                                                                                                                                                                                   |
| `400`  | `UNKNOWN_FIELD`                   | The body carries a field TokPortal does not recognise. This write is a full replace, so an unrecognised name is refused instead of dropped. `details.unknown_fields`, `details.did_you_mean` and `details.accepted_fields` say what to fix.                                |
| `400`  | `YOUTUBE_DELAYED`                 | `platform: "youtube"`. Bundle creation is TikTok and Instagram only, even though `GET /credit-costs` lists a YouTube price.                                                                                                                                                |
| `400`  | `ADVANCED_WARMING_FLAG_REQUIRED`  | `advanced_warming_terms` and/or `advanced_warming_terms_count` was sent without `wants_advanced_warming: true` (absent or explicitly `false`). Nothing is charged. `details.supplied_fields` lists the fields that triggered it.                                           |
| `400`  | `ADVANCED_WARMING_TERMS_REJECTED` | A term was removed by cleanup — a case-insensitive duplicate, or outside 2-50 characters. `details.dropped_terms` lists them and `details.accepted_terms` shows what survived. TokPortal refuses the call rather than delivering and billing fewer targets than requested. |
| `400`  | `ADVANCED_WARMING_COUNT_MISMATCH` | `advanced_warming_terms` and `advanced_warming_terms_count` state different quantities. Compare `details.terms_count` and `details.requested_count`; send one or the other.                                                                                                |
| `400`  | `ADVANCED_WARMING_TERMS`          | Provide `advanced_warming_terms` (3-30 terms, multiple of 3, each 2-50 chars) or `advanced_warming_terms_count` (3-30, multiple of 3).                                                                                                                                     |
| `400`  | `ADVANCED_WARMING_PLATFORM`       | Advanced Niche Warming is only available for TikTok and Instagram.                                                                                                                                                                                                         |
| `400`  | `WARMING_CONFLICT`                | Advanced Niche Warming cannot be combined with the legacy `wants_niche_warming` flag.                                                                                                                                                                                      |
| `400`  | `DEEP_WARMING_DEPRECATED`         | Legacy: `wants_deep_warming: true` on a new order. Use Advanced Niche Warming.                                                                                                                                                                                             |
| `400`  | `EDITS_EXCEED_VIDEOS`             | `edits_quantity` exceeds `videos_quantity`.                                                                                                                                                                                                                                |
| `400`  | `COUNTRY_NOT_ENABLED`             | Country is not enabled for your organization.                                                                                                                                                                                                                              |
| `400`  | `ACCOUNT_ID_NOT_ALLOWED`          | `account_id` was passed with a `bundle_type` other than `videos_only`. Set `bundle_type` to `videos_only` to add videos to an existing account, or omit `account_id`.                                                                                                      |
| `402`  | `INSUFFICIENT_CREDITS`            | Not enough credits to create this bundle.                                                                                                                                                                                                                                  |
| `404`  | `ACCOUNT_NOT_FOUND`               | `account_id` does not exist or does not belong to your organization.                                                                                                                                                                                                       |
| `409`  | `MANAGED_ACCOUNT_TASK_BLOCKED`    | A `videos_only` account has inactive or terminal Coverage. Read `details.reason`, fetch the account Coverage snapshot, and reactivate only when the state is recoverable. No bundle or debit is created.                                                                   |
| `409`  | `DUPLICATE_ACCOUNT_BUNDLE`        | A new-account bundle with this `external_ref` already exists, or the short no-reference duplicate guard matched. Inspect `details.existing_bundle_id`. This is detection, not an idempotent replay.                                                                        |
| `409`  | `BUNDLE_PRICING_CHANGED`          | The authoritative price or contract allowance changed before checkout. Nothing was created or charged. Fetch `GET /credit-costs` again and retry with a new `Idempotency-Key`.                                                                                             |

## FAQ

### Can I comment or reply to comments?

Yes — but **not** through bundle creation. Commenting is its own product, the **[Comments API](https://developers.tokportal.com/comments-overview)**, separate from bundles.

From any of your **delivered accounts with active TokPortal Coverage, or a permanently grandfathered account**, you can:

- **Comment on a video** — post a comment under any TikTok/Instagram video. Set `target_type: "video"` and pass `target_video_url`.
- **Reply to a specific comment** — reply under a particular comment on a TikTok video (TikTok only). Set `target_type: "comment"` and pass `target_comment_url` (the link from the app's *Share comment → Copy link*).

The assigned account manager posts it, then TokPortal **auto-verifies** the comment/reply by scraping the target (matching your account handle + text). Each comment costs **1 credit**, debited up-front and refunded if it can't be fulfilled.

```bash
curl -X POST https://app.tokportal.com/api/ext/comments \
  -H "X-API-Key: sk_xxx" \
  -H "Idempotency-Key: comment-task-example-v1" \
  -H "Content-Type: application/json" \
  -d '{
    "saved_account_id": "9f3a7b2e-1c4d-4e8f-a5b6-7d9e0f1a2b3c",
    "target_type": "comment",
    "target_comment_url": "https://www.tiktok.com/t/ZT9j5AXVsyuRS/",
    "comment_text": "100% agree with this!"
  }'
```

See the full **[Comments API reference](https://developers.tokportal.com/comments-overview)** for lifecycle, verification, and batch usage.

> The old per-bundle "community management" add-on (`wants_moderation`) is **deprecated** in favor of this Comments API.
