Create Bundle - Provision Accounts & Videos
Create a new TokPortal bundle via API to provision accounts and video slots for TikTok or Instagram campaigns.
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. |
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. 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 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 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. |
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, which lets you post comments and reply to specific comments from your managed accounts.wants_moderation/moderation_noticeare 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 resultingsaved_account_id; reusing that account in later bundles never creates another record. A bundle inpending_setuporpublishedcannot activate Coverage. Once activated, bundle completion, cancellation, or inactivity does not stop it.
Validation Rules
platformmust betiktokorinstagram.youtubeis rejected withYOUTUBE_DELAYED.wants_advanced_warming: trueis mandatory wheneveradvanced_warming_termsoradvanced_warming_terms_countis present. TokPortal never infers it: targets sent without the flag (absent or explicitlyfalse) are refused withADVANCED_WARMING_FLAG_REQUIREDand nothing is charged.wants_advanced_warmingrequires eitheradvanced_warming_terms_countoradvanced_warming_terms(3-30 targets, multiples of 3), otherwiseADVANCED_WARMING_TERMS. The standard minimum warming charge is 15 credits; use the activecontract_bundle_allowancewhen present. TikTok and Instagram only (ADVANCED_WARMING_PLATFORM).- Every term in
advanced_warming_termsmust 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 withADVANCED_WARMING_TERMS_REJECTEDanddetails.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_termsandadvanced_warming_terms_count, they must agree. A disagreement fails withADVANCED_WARMING_COUNT_MISMATCH; neither quantity silently wins. edits_quantitycannot exceedvideos_quantity.countrymust be available for new account creation. Disabled countries and countries restricted to existing-accountvideos_onlywork are rejected.USandGBaliases 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.
NOTE: Legacy warming flags
wants_niche_warming,wants_deep_warmingandniche_warming_instructionsare kept only for accounts being re-created after a refund. Their behaviour is unchanged:wants_niche_warmingandwants_advanced_warmingremain mutually exclusive (WARMING_CONFLICT),niche_warming_instructionsis still required whenwants_niche_warmingistrue, andwants_deep_warming: trueon a new order is still rejected withDEEP_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
{
"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)
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: trueis not optional here. Sendingadvanced_warming_termson its own is refused withADVANCED_WARMING_FLAG_REQUIRED— the flag is what is priced and what creates the warming session.
Response:
{
"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)
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:
{
"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-termswithadvanced_warming_terms. See the Advanced Niche Warming guide.
3. Videos Only (existing account)
Recommendation: Instead of creating a
videos_onlybundle, consider using the add-video-slots endpoint on an existing active bundle. This preserves the same account manager, which is better for continuity and turnaround time.
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:
{
"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:
platformandcountryare derived from the saved account when usingvideos_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, 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 passtarget_video_url. - Reply to a specific comment — reply under a particular comment on a TikTok video (TikTok only). Set
target_type: "comment"and passtarget_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.
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 for lifecycle, verification, and batch usage.
The old per-bundle "community management" add-on (
wants_moderation) is deprecated in favor of this Comments API.