Comments

Create Comments – Single & Batch

Create a single comment task or batch up to 200 from one POST. Validation rules, response shape, and error handling.

Create Comments

POST /api/ext/comments

Create one comment task or up to 200 in a single call. Each accepted task costs 1 credit. Validation happens before billing; rejected rows are never debited. The accepted-task debit and all accepted task inserts commit atomically.

Send an Idempotency-Key header on every create call. Reuse the same key only for an exact retry with the same method, path and body.

Target types

Each task targets one of two things, set with target_type:

target_typeWhat the manager doesRequired URL fieldPlatforms
video (default)Posts a new comment under a videotarget_video_urlTikTok, Instagram
commentReplies to a specific comment under a videotarget_comment_urlTikTok only

If you omit target_type, it defaults to video (the legacy behaviour) — existing integrations keep working unchanged.

For a comment target, paste the comment deep-link copied from the TikTok app (Share comment → Copy link). Both the short form (https://www.tiktok.com/t/XXXX/) and the full form (...?share_comment_id=...) work — the link is resolved and validated server-side at no extra cost (no scraping).

NOTE: Short links are best-effort TikTok occasionally bounces short comment-links (t/…) when resolved from a server, so we can't always extract the exact comment id up-front. When that happens the task is still created (we trust your explicit comment choice) and auto-verification runs best-effort. Make sure your link actually opens a comment. If it doesn't, the assigned manager flags it as "not a comment" → the task is cancelled and your credit is refunded. Passing the full ...?share_comment_id=... URL always resolves cleanly and enables exact verification.

Single task

Send a single object:

curl -X POST https://app.tokportal.com/api/ext/comments \
  -H "X-API-Key: sk_xxx" \
  -H "Idempotency-Key: comments-create-018f4a1a" \
  -H "Content-Type: application/json" \
  -d '{
    "saved_account_id": "9f3a7b2e-1c4d-4e8f-a5b6-7d9e0f1a2b3c",
    "target_video_url": "https://www.tiktok.com/@someone/video/7000000000000000000",
    "comment_text": "Love this technique ✨"
  }'

Success (201):

{
  "data": {
    "created": [
      {
        "id": "0d8b5a3e-92c4-4111-9a7d-3e2f1a2b3c4d",
        "status": "pending",
        "platform": "tiktok",
        "saved_account_id": "9f3a7b2e-1c4d-4e8f-a5b6-7d9e0f1a2b3c",
        "target_type": "video",
        "target_video_url": "https://www.tiktok.com/@someone/video/7000000000000000000",
        "target_comment_url": null,
        "target_comment_id": null,
        "target_author_handle": "someone",
        "comment_text": "Love this technique ✨",
        "cm_payout_amount": 0.15,
        "submitted_at": null,
        "verified_at": null,
        "manually_confirmed_at": null,
        "client_dispute_deadline_at": null,
        "finalized_at": null,
        "correction_required": null,
        "deadline_at": "2026-04-30T17:21:00Z",
        "execution_blocked": false,
        "execution_blocked_at": null,
        "execution_block_reason": null,
        "created_at": "2026-04-27T17:21:00Z",
        "updated_at": "2026-04-27T17:21:00Z"
      }
    ],
    "rejected": []
  },
  "created_count": 1,
  "rejected_count": 0,
  "credits_charged": 1
}

On a single-task call, validation errors return a typed 4xx instead of a rejected array, so you can branch on error.code without parsing the body twice. See error codes.

Reply to a comment (comment target)

Set target_type: "comment" and pass target_comment_url instead of target_video_url:

curl -X POST https://app.tokportal.com/api/ext/comments \
  -H "X-API-Key: sk_xxx" \
  -H "Idempotency-Key: comments-reply-018f4a1b" \
  -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!"
  }'

The resolved task echoes both the underlying video URL and the comment it targets:

{
  "data": {
    "created": [
      {
        "id": "7b1c...",
        "status": "pending",
        "platform": "tiktok",
        "target_type": "comment",
        "target_video_url": "https://www.tiktok.com/@someone/video/7650858712323656974",
        "target_comment_url": "https://www.tiktok.com/@someone/video/7650858712323656974?share_comment_id=7651510351893316372",
        "target_comment_id": "7651510351893316372",
        "target_author_handle": "someone",
        "comment_text": "100% agree with this!",
        "...": "..."
      }
    ],
    "rejected": []
  },
  "created_count": 1,
  "rejected_count": 0,
  "credits_charged": 1
}

Batch (up to 200)

Wrap in { "tasks": [...] }:

curl -X POST https://app.tokportal.com/api/ext/comments \
  -H "X-API-Key: sk_xxx" \
  -H "Idempotency-Key: comments-batch-018f4a1c" \
  -H "Content-Type: application/json" \
  -d '{
    "tasks": [
      {
        "saved_account_id": "9f3a7b2e-1c4d-4e8f-a5b6-7d9e0f1a2b3c",
        "target_video_url": "https://www.tiktok.com/@someone/video/7000000000000000000",
        "comment_text": "Brilliant idea"
      },
      {
        "saved_account_id": "b2c3d4e5-6f7a-8b9c-0d1e-2f3a4b5c6d7e",
        "target_video_url": "https://www.instagram.com/reel/AbCdEf12345/",
        "comment_text": "Stunning shot 🌟"
      },
      {
        "saved_account_id": "9f3a7b2e-1c4d-4e8f-a5b6-7d9e0f1a2b3c",
        "target_video_url": "ftp://broken.example",
        "comment_text": "Cool"
      }
    ]
  }'

Partial-success response (201):

{
  "data": {
    "created": [
      { "id": "...", "status": "pending", "...": "..." },
      { "id": "...", "status": "pending", "...": "..." }
    ],
    "rejected": [
      {
        "reason": "invalid_video_url",
        "raw": {
          "saved_account_id": "9f3a7b2e-1c4d-4e8f-a5b6-7d9e0f1a2b3c",
          "target_video_url": "ftp://broken.example"
        }
      }
    ]
  },
  "created_count": 2,
  "rejected_count": 1,
  "credits_charged": 2
}

The batch path always returns 201, even when every row fails. Read created_count, rejected_count, and the authoritative credits_charged value. Retry only recoverable rejected rows as a new logical request with a new idempotency key.

Request body

FieldTypeRequiredDescription
saved_account_idstring (UUID)yesA delivered account you own. Find IDs via GET /api/ext/accounts.
target_typestringnovideo (default) or comment. See Target types.
target_video_urlstringconditionalRequired when target_type is video (or omitted). TikTok / Instagram video URL. The parser is lenient — protocol, vm.tiktok.com/..., tiktok.com/t/..., IG /share/..., instagram.com/reel/..., all work.
target_comment_urlstringconditionalRequired when target_type is comment. TikTok comment deep-link (Share comment → Copy link); short t/… or full ?share_comment_id=… links both work. TikTok only.
comment_textstringyesThe exact text the manager will post. Length capped per platform (TikTok 150, IG 2200).
brief_idstring (UUID)noOptional internal grouping ID — see your dashboard.

For the batch form, wrap in { "tasks": [ ... ] } (1–200 entries). You can mix video and comment targets in the same batch.

Validation rules

The same rules apply to single and batch:

  1. saved_account_id must belong to your user (SAVED_ACCOUNT_NOT_OWNED otherwise).
  2. The account must have active TokPortal Coverage or be permanently grandfathered. Paused, lapsed, detached, banned, missing or unrecoverable Coverage states cannot create executable work.
  3. The account must have an active manager (current_cm_id IS NOT NULL). If not, you'll see COMMENT_ACCOUNT_NOT_MANAGED (single) or account_not_managed (batch). Wait until an order is in progress on that account.
  4. For video targets, target_video_url must parse to one of the supported platforms. For comment targets, target_comment_url must be a TikTok link; it's rejected only if it clearly resolves to a video (not_a_comment_url). Otherwise it's accepted (best-effort, see note above).
  5. The target's platform must match the account's platform. Otherwise the API returns COMMENT_PLATFORM_MISMATCH (single) or platform_mismatch (batch). Comment targets require a TikTok account.
  6. comment_text length must be 1–150 (TikTok), 1–2200 (IG).

Per-row rejection reasons (batch path)

rejected[*].reasonMeaning
account_not_foundsaved_account_id doesn't exist.
account_not_ownedAccount belongs to a different user.
account_not_managedAccount has no current_cm_id — no manager can take this task.
managed_subscription_lapsedCoverage period ended. Fetch a fresh Coverage quote and reactivate before retrying.
managed_subscription_cancelledCoverage was manually paused. Fetch the current period and reactivation quote.
managed_subscription_missingA post-cutoff eligible account has no usable Coverage record. Do not treat it as grandfathered; contact support with the request ID.
account_revealedCredentials were revealed and the account is permanently detached. Do not retry.
account_bannedThe account is banned. Do not create new work.
managed_account_unrecoverableCoverage cannot be reactivated for this account. Do not retry.
managed_account_access_lookup_failedCoverage could not be verified. No task or debit was created; retry with the same idempotency key.
invalid_video_urltarget_video_url didn't parse.
invalid_comment_urltarget_comment_url is not a recognizable TikTok comment link.
not_a_comment_urlThe link resolved to a video, not a specific comment — use a video target.
comment_target_platform_unsupportedComment targets are TikTok-only.
platform_mismatchTarget platform ≠ account platform.
comment_text_too_longExceeded the platform's max chars.
comment_text_length_0_exceeds_XText is empty (legacy literal).
insufficient_creditsThe accepted subset could not be paid. No accepted task or debit was committed; top up and retry as a new logical request.

Tips

  • Idempotency: send an Idempotency-Key and reuse the same key, method, path, and body for an exact transport retry. TokPortal replays a durably completed response for 24 hours. If IDEMPOTENCY_KEY_IN_PROGRESS persists, stop instead of changing keys and ask support to reconcile the uncertain claim; it never auto-expires into another execution. For a genuinely new logical call or a changed body, use a new key and still dedupe locally on (saved_account_id, target_video_url || target_comment_url, comment_text). Different keys for identical comments represent different tasks and can bill twice.
  • Don't post under-72h windows where you can't act. A pending task you don't follow up on auto-cancels at 72h and refunds, but you also burn a manager's mindshare.
  • Test on a single task first. The single-shot path returns precise typed errors, easier to debug than a partial batch.
  • CSV import? The dashboard's CSV importer hits the same engine — every rule above applies there too.