Bundles

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

FieldTypeDescription
bundle_typestring"account_only", "account_and_videos", or "videos_only".

Conditional

FieldTypeRequired WhenDescription
countrystringaccount_only, account_and_videosTokPortal 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_idstring (UUID)videos_onlyThe 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_quantityintegeraccount_and_videos, videos_onlyNumber of video slots. Must be greater than 0.

Optional

FieldTypeDefaultDescription
platformstring"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.
titlestringBundle title. Max 200 characters.
edits_quantityinteger0Number of video edits. Cannot exceed videos_quantity.
wants_advanced_warmingbooleanfalseEnable 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_countintegerNumber 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_termsstring[]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_warmingbooleanfalse⚠️ Deprecated. Legacy — kept only for accounts being re-created after a refund; use Advanced Niche Warming (wants_advanced_warming).
wants_deep_warmingbooleanfalse⚠️ Deprecated. Legacy — kept only for accounts being re-created after a refund; use Advanced Niche Warming (wants_advanced_warming).
wants_moderationbooleanfalse⚠️ 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_instructionsstring⚠️ Deprecated. Legacy — kept only for accounts being re-created after a refund; use Advanced Niche Warming (wants_advanced_warming).
moderation_noticestring⚠️ Deprecated — no longer applied. Was the guidelines notice for community management. Use the Comments API instead.
auto_finalize_videosbooleantrueDelivered 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_refstring-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_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.

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

{
  "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
  }
}
FieldTypeDescription
data.bundle_idstringUnique identifier for the created bundle.
data.bundle_typestringThe bundle type as submitted.
data.platformstringTarget platform.
data.countrystringCountry code.
data.statusstringInitial bundle status.
data.videos_quantityintegerNumber of video slots (if applicable).
data.edits_quantityintegerNumber of edits (if applicable).
data.external_refstringYour external reference (if provided).
data.review_modestringauto 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_noticestringPresent 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_atstringISO 8601 timestamp.
credits_chargedintegerCredits deducted for this bundle.
credits_remainingintegerYour remaining credit balance.
cost_breakdownobjectItemized 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: 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:

{
  "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-terms with advanced_warming_terms. See the Advanced Niche Warming guide.

3. Videos Only (existing account)

Recommendation: Instead of creating a videos_only bundle, 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: platform and country are derived from the saved account when using videos_only.

Error Responses

StatusCodeDescription
400VALIDATION_ERRORInvalid or missing fields (see details for specifics).
400UNKNOWN_FIELDThe 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.
400YOUTUBE_DELAYEDplatform: "youtube". Bundle creation is TikTok and Instagram only, even though GET /credit-costs lists a YouTube price.
400ADVANCED_WARMING_FLAG_REQUIREDadvanced_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.
400ADVANCED_WARMING_TERMS_REJECTEDA 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.
400ADVANCED_WARMING_COUNT_MISMATCHadvanced_warming_terms and advanced_warming_terms_count state different quantities. Compare details.terms_count and details.requested_count; send one or the other.
400ADVANCED_WARMING_TERMSProvide advanced_warming_terms (3-30 terms, multiple of 3, each 2-50 chars) or advanced_warming_terms_count (3-30, multiple of 3).
400ADVANCED_WARMING_PLATFORMAdvanced Niche Warming is only available for TikTok and Instagram.
400WARMING_CONFLICTAdvanced Niche Warming cannot be combined with the legacy wants_niche_warming flag.
400DEEP_WARMING_DEPRECATEDLegacy: wants_deep_warming: true on a new order. Use Advanced Niche Warming.
400EDITS_EXCEED_VIDEOSedits_quantity exceeds videos_quantity.
400COUNTRY_NOT_ENABLEDCountry is not enabled for your organization.
400ACCOUNT_ID_NOT_ALLOWEDaccount_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.
402INSUFFICIENT_CREDITSNot enough credits to create this bundle.
404ACCOUNT_NOT_FOUNDaccount_id does not exist or does not belong to your organization.
409MANAGED_ACCOUNT_TASK_BLOCKEDA 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.
409DUPLICATE_ACCOUNT_BUNDLEA 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.
409BUNDLE_PRICING_CHANGEDThe 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 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.

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.