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_type | What the manager does | Required URL field | Platforms |
|---|---|---|---|
video (default) | Posts a new comment under a video | target_video_url | TikTok, Instagram |
comment | Replies to a specific comment under a video | target_comment_url | TikTok 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 explicitcommentchoice) 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
| Field | Type | Required | Description |
|---|---|---|---|
saved_account_id | string (UUID) | yes | A delivered account you own. Find IDs via GET /api/ext/accounts. |
target_type | string | no | video (default) or comment. See Target types. |
target_video_url | string | conditional | Required 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_url | string | conditional | Required 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_text | string | yes | The exact text the manager will post. Length capped per platform (TikTok 150, IG 2200). |
brief_id | string (UUID) | no | Optional 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:
saved_account_idmust belong to your user (SAVED_ACCOUNT_NOT_OWNEDotherwise).- The account must have active TokPortal Coverage or be permanently grandfathered. Paused, lapsed, detached, banned, missing or unrecoverable Coverage states cannot create executable work.
- The account must have an active manager (
current_cm_id IS NOT NULL). If not, you'll seeCOMMENT_ACCOUNT_NOT_MANAGED(single) oraccount_not_managed(batch). Wait until an order is in progress on that account. - For
videotargets,target_video_urlmust parse to one of the supported platforms. Forcommenttargets,target_comment_urlmust 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). - The target's platform must match the account's platform. Otherwise the API returns
COMMENT_PLATFORM_MISMATCH(single) orplatform_mismatch(batch). Comment targets require a TikTok account. comment_textlength must be 1–150 (TikTok), 1–2200 (IG).
Per-row rejection reasons (batch path)
rejected[*].reason | Meaning |
|---|---|
account_not_found | saved_account_id doesn't exist. |
account_not_owned | Account belongs to a different user. |
account_not_managed | Account has no current_cm_id — no manager can take this task. |
managed_subscription_lapsed | Coverage period ended. Fetch a fresh Coverage quote and reactivate before retrying. |
managed_subscription_cancelled | Coverage was manually paused. Fetch the current period and reactivation quote. |
managed_subscription_missing | A post-cutoff eligible account has no usable Coverage record. Do not treat it as grandfathered; contact support with the request ID. |
account_revealed | Credentials were revealed and the account is permanently detached. Do not retry. |
account_banned | The account is banned. Do not create new work. |
managed_account_unrecoverable | Coverage cannot be reactivated for this account. Do not retry. |
managed_account_access_lookup_failed | Coverage could not be verified. No task or debit was created; retry with the same idempotency key. |
invalid_video_url | target_video_url didn't parse. |
invalid_comment_url | target_comment_url is not a recognizable TikTok comment link. |
not_a_comment_url | The link resolved to a video, not a specific comment — use a video target. |
comment_target_platform_unsupported | Comment targets are TikTok-only. |
platform_mismatch | Target platform ≠ account platform. |
comment_text_too_long | Exceeded the platform's max chars. |
comment_text_length_0_exceeds_X | Text is empty (legacy literal). |
insufficient_credits | The 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-Keyand reuse the same key, method, path, and body for an exact transport retry. TokPortal replays a durably completed response for 24 hours. IfIDEMPOTENCY_KEY_IN_PROGRESSpersists, 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
pendingtask 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.