TikTok 发布全流程
上传视频 → 发布到 TikTok → 轮询发布状态
TikTok 发布全流程
流程总览
上传视频(拿 videoUrl,7 天有效)→ 发布(拿 shareId)→ 轮询状态(直到 publish_complete / publish_failed)背后发生的事:你提交发布后,TikTok 会自己来下载这个 URL(不需要你的服务器长期在线)→ 转码 → 内容审核 → 上架。所以「发布」是异步的,提交成功 ≠ 发布成功,一定要轮询到终态。
新版发布接口(1.5.x 起默认):TikTok 发布默认走新版接口;CLI 加
--via beervid走的是旧版接口(将要废弃——1.6.x 移除,移除前功能不受影响)。两版的参数、返回字段(publishId/shareId/status)、状态轮询(publish status --tt --publish-id,--share-id为兼容别名)完全一致,对用户无感切换。
📉 官方提醒:发布后视频的清晰度和比特率可能比原文件略低——这是 TikTok 转码时的正常现象(取决于播放设备和网络),不是你的视频坏了。
下文示例中
$BASE为生产环境地址(https://cli.haina.io),$TOKEN为「API Keys」页创建的 API Token。
第 1 步:上传视频
curl -X POST -H "Authorization: Bearer $TOKEN" \
-F "file=@demo.mp4" \
$BASE/v1/videos返回 201:
{
"code": 0,
"data": {
"videoId": "01a08b67-4422-78d-a3a214-0c0279fc",
"videoUrl": "https://sm-cdn-assets.beervid.ai/....mp4",
"expiresAt": "2026-09-17T06:40:39.716Z",
"filename": "demo.mp4",
"sizeBytes": 10904
}
}约束:mp4/mov,≤100MB(平台侧限制)。videoUrl 7 天有效,到期后该视频不可再用于发布(2005),需重新上传。
视频规则汇总(平台约束 + 官方硬性规格,不满足会在上传/发布阶段失败):
| 项 | 规则 |
|---|---|
| 格式 | mp4 / mov(Content-Type video/mp4 / video/quicktime;官方还支持 webm,本平台只收前两种) |
| 大小 | ≤ 100MB(超限 2001;官方上限 1GB,本平台收紧) |
| TikTok 视频官方规格 | 时长 3–600 秒;宽高均 ≥360px;帧率 23–60 FPS(不满足会在发布阶段被官方拒) |
| TikTok 视频有效期 | videoUrl 上传后 7 天有效(平台计时);到期不可发布(2005),需重新上传 |
| TikTok Shop 视频 | fileId 一次性消耗:发布调用即作废(失败也不可复用),重发需重新上传;无 URL 过期概念 |
| 发布频次 | TikTok 发布:官方限制每账号 ≤6 个/分钟、≤15 个/天(批量发布请自行控制节奏,超限会被官方拒绝);TikTok Shop 挂车:平台侧配额(不同账号不同),超限官方报错,发布成功响应可能带 quota 字段提示剩余配额 |
| TikTok Shop 预审配额 | 50 次/天/账号(API 渠道独立计数;同一 fileId 重复提交会多次消耗);耗尽返回 2002,次日恢复 |
第 2 步:发布
curl -X POST -H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"videoUrl": "https://sm-cdn-assets.beervid.ai/....mp4",
"accountId": "<坐席里 TikTok 绑定的 businessId>",
"caption": "视频文案 #话题",
"disableComment": false
}' \
$BASE/v1/publish/tiktokvideoUrl 就是第 1 步上传响应里的 videoUrl 原值——平台会校验它属于你的视频库且在 7 天有效期内(查无 1005,过期 2005)。
可选字段均有默认值,按需覆盖即可:caption(视频文案,≤2200 字符,支持 #话题 和 @提及——注意 @提及 仅对互关好友生效,且在 App 内显示为纯文本)、isBrandOrganic / isBrandedContent(品牌标记,默认 false)、disableComment / disableDuet / disableStitch(互动开关,默认 false = 不禁用)、封面二选一(thumbnailOffset 视频内取帧毫秒,默认 0 = 首帧,别填超过视频时长的值,官方会直接判发布失败;customThumbnailUrl 自定义图片封面——用 POST /v1/images 上传拿到的 imageUrl,传了它 thumbnailOffset 被官方忽略)。逐字段说明见 API 参考 · POST /v1/publish/tiktok。
返回 201:
{
"code": 0,
"data": {
"publishId": "01a08b67-845d-77d-bc0938-aa21d13e",
"capability": "TT",
"shareId": "v_pub_url~v2.7683890605927450638",
"status": "publish_complete"
}
}记住
shareId——查状态要用。每次发布都会自动记入后台「发布记录」页,随时可查。 状态值已归一化(小写):submitted→publish_processing→publish_complete/publish_failed(官方错误为beervid_error)。
第 3 步:轮询状态
curl -H "Authorization: Bearer $TOKEN" \
"$BASE/v1/tiktok/v1.3/business/publish/status?publishId=v_pub_url~v2.7683890605927450638"
publishId即第 2 步响应的shareId(官方 publish_id)。命中你的发布记录时,每次查询都会顺带更新发布记录(归属校验 + 回写自动完成)。旧路径GET /v1/publish/tiktok/status?shareId=将要废弃(1.6.x 移除)。
{
"code": 0,
"data": {
"publishId": "…",
"shareId": "v_pub_url~v2.7683890605927450638",
"status": "publish_complete",
"postId": "7234567890123",
"beervid": { "status": "PUBLISH_COMPLETE", "post_ids": ["7234567890123"] }
}
}建议轮询间隔 5–10 秒,最多 ~30 次,到终态(publish_complete / publish_failed / beervid_error)即停。每次查询都会顺带更新你的发布记录。
几个实用细节:
postId(TikTok item_id)在publish_complete后自动写入发布记录——但官方的数据处理可能延迟约 3 分钟,刚成功没拿到postId时过几分钟再查一次即可。- 拿到
postId就能做数据回收(见第 4 步)。 - 发布失败时,失败原因在响应的
beervid.reason(官方原因码,如frame_rate_check_failed= 帧率不合规,改视频参数后重传再发)。 - 官方状态里还可能出现
SEND_TO_USER_INBOX(草稿箱场景)——本平台不开放草稿发布,正常不会遇到,看到它说明官方把帖子放进了创作者收件箱。
第 4 步(可选):取视频数据
发布成功后,拿 status 响应里的 postId(TikTok item_id)拉播放/互动指标——走「数据洞察」面(与官方接口路由一致):
curl -H "Authorization: Bearer $TOKEN" \
"$BASE/v1/tiktok/v1.3/business/video/list?account=<businessId>&videoIds=7234567890123"返回 data.videos[] 为官方原样字段(video_views/likes/comments/shares/reach 等,字段可得性取决于授权 scope;可用 fields 自选字段集)。account 为坐席里 TikTok 绑定的 businessId 或 username。
旧端点
GET /v1/publish/stats?itemId=(将要废弃,1.6.x 移除)仍可用,但请优先用上面的新版路由。
图片帖发布
图片帖(photo post)走新版端点(旧版无此能力):
haina tt photo publish --account <businessId> \
--photo-urls "https://…/a.jpg,https://…/b.jpg" \
--privacy-level PUBLIC_TO_EVERYONE \
--title "标题" --caption "描述" --wait关键口径(官方文档实证):
photo_urls1-35 张公网 https 图(每张 ≤20MB,JPG/JPEG/WebP);--privacy-level必填(可用档位先查haina tt insight video-settings --account …的privacy_level_options)。--draft发草稿(进 TikTok App 收件箱,坐席授权需含 video.upload scope)。- 配额硬限(图片与视频同额):每账号 6 帖/分钟、15 帖/天;超限官方报错透传。
- URL 所有权验证:2023-11 起未验证域名拒发;Beervid OSS 域名已验证,自有域名需在 TikTok 侧先做 URL 资源所有权验证。
- 状态轮询与视频完全同口径(
publish status --tt --publish-id,草稿终态为publish_draft_inbox)。
发布辅助(写 caption / 挂地域标签前查询):
haina tt hashtag suggest --account <id> --keyword summer # 话题推荐(10 条,含播放量)
haina tt location search --account <id> --query "new york" # 地域标签(id+name 配对用于 --location-id/--location-name)BGM:给视频挂官方商用音乐(CML)
视频帖和图片帖都可以挂 TikTok 官方商用音乐库(CML)的 BGM,分两步:先选曲,再发布。
选曲(discovery music)
haina tt music trending --account <id> --genre POP --country US --date-range 7DAY返回 ≤100 条热门曲目(可按流派/地区/时间窗筛选)。发布要用的 ID 是表格里的 trendingSongClipId(近 30 天最热剪辑)或 fullSongClipId(完整曲目)——不是 commercialMusicId(那是曲目 id,仅供选曲参考查询):
--json消费的取值路径:songClipId 在嵌套对象里——data.list[].trendingSongClip.songClipId/data.list[].fullDurationSongClip.songClipId(表格两列是它们的 flatten 展示)。注意fullDurationSongClip可能为空对象{}(此时只能用 trendingSongClip);曲目顶层duration对剪辑曲目可能为 0,真实时长看 clip 对象的duration。
haina tt music trending-videos --account <id> --commercial-music-id <曲目id> # 看这首曲子的热门视频(选曲参考)试听直接打开响应里的 previewUrl(不过期)。
发布时挂 BGM
haina tt video publish --video-url <url> --account <id> \
--music-id <trendingSongClipId> \
--music-volume 50 --original-sound-volume 100 \
--wait图片帖指定曲目:haina tt photo publish ... --music-id <songClipId>(自动配乐用 --auto-add-music)。
必读的三个坑
- 音量默认是静音:官方 API 音乐音量和原声音量默认都是 0(静音),App 内默认是 50。本平台对齐 App——传了
--music-id但没给音量时自动按 50/50;显式传 0 以你为准。要保留视频原声(口播人声)必须--original-sound-volume> 0。注意这与 TikTok Shop 挂车的--music-id(电商库)语义不同:挂车侧是完全覆盖原声,CML 是混音(音乐/原声两个音量滑杆)。 - CML ≠ App 流行曲库:热门版权流行歌不对 Business 账号开放(App 内也一样)。要流行歌只能发草稿(
--draft)后在 App 内手动加。 - 音乐裁剪:
--music-start/--music-end毫秒;不传--music-end默认取到视频时长(10s 音乐配 2s 视频只放前 2s)。
查询发布记录
curl -H "Authorization: Bearer $TOKEN" \
"$BASE/v1/publish/records?capability=TT&page=1&pageSize=20"