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 task costs 1 credit, debited up-front; rejected rows are refunded automatically.
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 "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",
"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 "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 "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 to drive your retry logic.
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 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
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. |
account_lookup_failed | DB hiccup — retry the row. |
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 | Wallet ran out mid-batch. Top up and retry the rejected rows. |
Tips
- Idempotency: if you must retry a batch, dedupe locally on
(saved_account_id, target_video_url || target_comment_url, comment_text)first — TokPortal does not deduplicate identical calls. The same comment posted twice will create two pending tasks and bill two credits. - 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.