Bundles – Campaign Management API
List, filter, and retrieve bundle details via the TokPortal API. Bundles group accounts and video slots for campaign management.
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.
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 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_reviewwithout user approval or a correction request are automatically moved tofinalizedafter 72 hours. The window starts when the individual listing entersin_review, not when the bundle was created or published.
Platforms
| Platform | Value |
|---|---|
| TikTok | tiktok |
instagram |
Options
| Option | Description | Restrictions |
|---|---|---|
wants_advanced_warming | Enable advanced warming (recommended): search-term based warming with recorded proof and a data report. 5 credits per term after the workspace transition. | TikTok/Instagram only. Requires advanced_warming_terms or advanced_warming_terms_count (3-30, multiples of 3). Cannot combine with wants_niche_warming. |
advanced_warming_terms | The search terms for advanced warming. | 3-30 terms (count must be a multiple of 3), each 2-50 characters. |
advanced_warming_terms_count | Buy a target count now and configure the targets later. | 3-30, multiple of 3. Configure once with PUT /bundles/:id/warming-terms; unconfigured purchases are refunded after 14 days. |
wants_niche_warming | Enable niche warming (review-based; will be deprecated in favor of advanced warming). | Cannot combine with wants_advanced_warming; requires niche_warming_instructions. |
wants_deep_warming | ⚠️ Discontinued — rejected. New orders fail with DEEP_WARMING_DEPRECATED. Use advanced warming instead. | — |
wants_moderation | ⚠️ Deprecated — ignored. Replaced by the Comments API. | — |
edits_quantity | Add edit slots. | Cannot exceed videos_quantity. |
auto_finalize_videos | Automatically finalize videos once ready. | — |
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 bysaved_account_id, notbundle_id. Bulk creation, single creation, dashboard creation, API creation and either MCP transport all converge on the same rule. Additionalvideos_onlybundles 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 |
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 |
POST | /bundles/:id/publish | Publish a bundle |
POST | /bundles/:id/unpublish | Unpublish a bundle |
PUT | /bundles/:id/warming-terms | Configure a deferred 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— callPUT /bundles/:id/accountto set the account fields.configure_videos— callPUT /bundles/:id/videos/batch(or per-position) to fill the video slots.publish_bundle— callPOST /bundles/:id/publish(or passauto_publish: trueon 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.remadewebhook 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 + bundle.cancelled 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
# 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
{
"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": true,
"wants_niche_warming": true,
"wants_deep_warming": 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
curl -X GET https://app.tokportal.com/api/ext/bundles/bnd_abc123 \
-H "X-API-Key: sk_xxx"
Response
{
"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_niche_warming": true,
"wants_deep_warming": 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_idvsaccount.idGET /bundles/:idreturnssaved_account_id(top-level and insideaccount) — the id of the real created account, i.e. whatGET /accounts/{id}uses. It isnulluntil the account exists, and changes after a remake. Theaccount.idfield is the account listing id (the bundle's account slot), which is stable across remakes. Usesaved_account_idwhenever 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 seeaccount.finalized, or read it straight from theaccount.in_review/account.finalizedwebhooks.
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. |
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
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
{
"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
curl -X GET https://app.tokportal.com/api/ext/bundles/bnd_abc123/publish-readiness \
-H "X-API-Key: sk_xxx"
Response — ready
{
"data": {
"bundle_id": "bnd_abc123",
"ready": true,
"blockers": []
}
}
Response — blockers present
{
"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). |