短视频解析接口文档
一、接入流程
| 步骤 | 说明 |
|---|---|
| 1. 开通 | 联系商务,提供服务器出口 IP(或申请签名密钥) |
| 2. 充值 | 购买次数包,到账即可调用 |
| 3. 联调 | 用 /api/v2/parse 跑通一条抖音链接 |
| 4. 上线 | 按需加缓存与重试(失败不计费,可放心重试) |
开通后会拿到两样东西:
- AppID:如
ak_1aea664195 - Secret:40 位随机串,只在创建时显示一次,丢失只能重置
二、鉴权
系统支持两种方式,开通时确定一种即可,调用方无感。
方式 A:IP 白名单(老客户沿用,最省事)
- 把服务器出口 IP 报给商务,服务端按来源 IP 识别身份,请求里不用带任何鉴权参数
- 只认 TCP 层真实来源 IP,不识别
X-Forwarded-For等请求头——伪造头部无效 - 换机器 / 加机器记得提前报备 IP
方式 B:HMAC 签名(推荐,可跨机器、可防重放)
每个请求带 4 个 Header:
| Header | 必填 | 说明 |
|---|---|---|
X-AppId | 是 | 你的 AppID |
X-Timestamp | 是 | 秒级 Unix 时间戳,与服务器偏差不超过 300 秒 |
X-Nonce | 是 | 随机串,至少 8 位,同一 nonce 5 分钟内只能用一次 |
X-Sign | 是 | 签名值,小写十六进制 |
签名算法
待签串 = appid + "\n" + timestamp + "\n" + nonce + "\n" + md5(原始请求体)
sign = HMAC_SHA256(待签串, secret) # 输出小写 hex
1.
md5 必须对实际发出的原始 body 字节计算,不是重新序列化后的结果;body 为空时取 md5("")。2. 四段之间是换行符
\n,不是 & 也不是空串。3.
X-Sign 必须转小写再比较。
三、接口清单
| 方法 | 路径 | 用途 | 鉴权 |
|---|---|---|---|
| POST | /api/v2/parse | 解析短视频 / 图集(推荐) | 签名 / IP |
| GET | /api/v2/quota | 查询剩余次数与限额 | 签名 / IP |
| POST | /api/video | 老接口,向下兼容(支持视频号分享链接) | IP 白名单 |
四、解析接口 POST /api/v2/parse
请求
POST https://nowater.lsqykj.com/api/v2/parse
Content-Type: application/json
{
"url": "https://v.douyin.com/bU_ILkJDCNw/"
}
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
url | string | 二选一 | 作品链接。可传整段分享文案,服务端会自动提取 |
code | string | 二选一 | 只有短码时传这个(如抖音 iRNBho6G),必须同时传 platform |
platform | string | 否 | 平台提示,见附录 A。仅在传 code 或链接无法识别平台时需要 |
也支持 application/x-www-form-urlencoded 表单提交,字段名相同。
支持的输入格式
客户端拿到的往往不是干净链接。以下形态都能直接传,服务端会统一归一化:
| 形态 | 示例 |
|---|---|
| 完整链接 | https://v.douyin.com/iRNBho6G/ |
| 整段分享文案 | 7.68 复制打开抖音,看看【某某】的作品 https://v.douyin.com/iRNBho6G/ 复制此链接 |
| 缺协议头 | v.douyin.com/iRNBho6G/ |
| 协议相对 | //v.douyin.com/iRNBho6G/ |
| 被 URL 编码 | https%3A%2F%2Fv.douyin.com%2FiRNBho6G%2F |
| 被二次编码 | https%253A%252F%252Fv.douyin.com%252FiRNBho6G%252F |
| 带 HTML 实体 | https://v.douyin.com/iRNBho6G/?a=1&b=2 |
| 全角符号 | https://v.douyin.com/iRNBho6G/ |
| 带零宽字符 / emoji | https://v.douyin.com/iRNBho6G/ 🔥 |
| 只有短码 | {"code":"iRNBho6G","platform":"douyin"} |
· 文案里有多个链接时,优先挑已知平台的域名,避免抓到文案里的活动页、外链
· 链接尾部的
。,、!? 等中文标点自动去掉· 只有短码且没传
platform 会直接报错(code=2002)——服务端不猜平台,
猜错只会打出一个必然失败的请求· 短码拼接:抖音
v.douyin.com/{code}/、快手 v.kuaishou.com/{code}、
小红书 xhslink.com/a/{code}、B站 b23.tv/{code}
响应(成功)
{
"code": 0,
"msg": "ok",
"request_id": "ae3a20d19ddfac9ac4eca414a889de26",
"data": {
"platform": "douyin",
"type": "video",
"title": "拿不下你 心好痛 #蔚蓝 #日常vlog",
"cover": "https://p3-pc-sign.douyinpic.com/....jpeg",
"videos": ["https://v26-default.365yg.com/....mp4"],
"images": [],
"audio": "",
"duration": 15
},
"quota": {
"balance": 482310,
"charged": 1,
"cache_hit": 0
}
}
| 字段 | 类型 | 说明 |
|---|---|---|
code | int | 0 成功,非 0 见错误码表 |
msg | string | 成功固定 ok,失败为可读原因 |
request_id | string | 本次请求唯一号,报障时提供这个号 |
data.platform | string | 平台代码,见附录 A |
data.type | string | video 视频 / image 图集 |
data.title | string | 作品文案,可能为空 |
data.cover | string | 封面图直链,可能为空 |
data.videos | array | 视频直链,按清晰度从高到低;type=image 时为空数组 |
data.images | array | 图集直链;type=video 时为空数组 |
data.audio | string | 音轨直链,仅部分海外/音乐类链接返回,其余为空串 |
data.duration | int | 视频时长(秒),拿不到时为 0 |
quota.balance | int | 本次扣次后的剩余次数 |
quota.charged | int | 本次实际扣次(1 或 0) |
quota.cache_hit | int | 1 表示命中 30 分钟缓存 |
videos / images 里的地址是平台 CDN 的临时签名链接,
通常 1~24 小时后失效。需要长期保存请自行转存到自己的对象存储。响应(失败)
{
"code": 3001,
"msg": "解析失败,请稍后重试",
"request_id": "3fdd33f7141ba9d23c4d8eff61b3a1b4",
"data": null
}
五、余额查询 GET /api/v2/quota
{
"code": 0,
"msg": "ok",
"data": {
"balance_calls": 482310,
"total_calls": 284572,
"expire_at": null,
"daily_quota": 3000,
"qps_limit": 15
}
}
| 字段 | 说明 |
|---|---|
balance_calls | 剩余次数 |
total_calls | 累计成功调用次数 |
expire_at | 到期时间,null 表示永久 |
daily_quota | 每日上限,0 表示不限 |
qps_limit | 每秒请求上限 |
六、老接口(向下兼容)
已经在用旧版接口的客户,代码一行都不用改。响应结构、错误码、HTTP 状态码全部保持原样。
6.1 POST /api/video
POST /api/video
Content-Type: application/x-www-form-urlencoded
url=https://v.douyin.com/bU_ILkJDCNw/
{
"code": 200,
"msg": "success",
"data": {
"image": "https://...jpeg",
"title": "作品文案",
"isweibo": false,
"isyoutube": false,
"type": "single",
"images_url": "",
"video_url": "https://....mp4",
"audio_url": ""
}
}
| 字段 | 说明 |
|---|---|
image | 封面 |
title | 文案 |
isweibo / isyoutube | 平台标记,仅这两个平台为 true |
type | 固定 "single"(历史字段,不要用来判断类型) |
images_url | 图集时为数组,视频时为空字符串 ""(不是空数组) |
video_url | 视频时为直链,图集时为空字符串 |
audio_url | 音轨,通常为空 |
| code | 含义 |
|---|---|
-1 | 通用失败(含剩余次数不足、接口停用) |
-2 | url 链接不正确 |
-4 | 视频解析失败 |
code 判断结果。视频号走这个接口
微信视频号(https://weixin.qq.com/sph/xxxx)直接传分享链接即可,返回同样的老结构,
视频直链在 data.video_url,封面在 data.image:
curl -X POST https://nowater.lsqykj.com/api/video \
--data-urlencode "url=https://weixin.qq.com/sph/AsQc8pkmcK"
6.2 POST /api/sphvideo(微信视频号)— 已下线
视频号接口已停止服务。历史调用会收到:
{"code": -1, "msg": "视频号接口已下线", "data": ""}
原有客户端无需改动,但只会拿到 code=-1。如需重新开通请联系商务。
七、计费规则
| 场景 | 是否扣次 |
|---|---|
| 解析成功 | ✅ 扣 1 次 |
| 命中缓存(同链接 30 分钟内重复请求) | ✅ 扣 1 次 |
| 下游失败 / 上游超时 | ❌ 不扣,自动退回 |
| 参数错误、签名错误、余额不足、超限流 | ❌ 不扣 |
| 平台不支持 / 平台临时关闭 | ❌ 不扣 |
- 扣次是预扣 + 失败回滚,失败请求不消耗额度,可放心重试。
- 每次扣减 / 退回都会留一条流水(
consume/refund),可按笔对账。 - 余额为 0 时返回
code=1007、HTTP402。
八、限流
| 项 | 说明 |
|---|---|
| QPS | 按密钥维度限制(默认 10~15 次/秒),可单独调整 |
| 日额度 | 按密钥维度限制(默认 3000 次/天,0 表示不限) |
| 超出 QPS | HTTP 429,code=1009 |
| 超出日额度 | HTTP 429,code=1010 |
建议的客户端策略:并发控制在 5 以内;收到 429 后按指数退避重试(1s → 2s → 4s),不要原地重打。
九、错误码表
| code | HTTP | 含义 | 是否扣次 | 处理建议 |
|---|---|---|---|---|
0 | 200 | 成功 | 是 | — |
1001 | 401 | 签名参数缺失 | 否 | 检查 4 个 Header 是否都带上 |
1002 | 401 | 签名错误 | 否 | 核对签名串拼接与 secret |
1003 | 401 | 时间戳过期 | 否 | 校准服务器时间(NTP) |
1004 | 401 | nonce 重复 | 否 | nonce 用随机串,不要复用 |
1005 | 401 | 密钥不存在或已停用 | 否 | 联系商务 |
1006 | 401 | 密钥已到期 | 否 | 续期 |
1007 | 402 | 剩余次数不足 | 否 | 充值 |
1008 | 401 | IP 不在白名单 | 否 | 报备出口 IP |
1009 | 429 | 超出 QPS | 否 | 降并发 / 退避重试 |
1010 | 429 | 超出日额度 | 否 | 次日再试或提额 |
2001 | 400 | 平台不支持 / 已关闭 | 否 | 换链接或联系开通 |
2002 | 400 | url 参数非法 | 否 | 检查链接 |
3001 | 502 | 上游解析失败 | 否 | 可重试 1~2 次 |
3002 | 503 | 暂无可用通道 | 否 | 稍后重试 |
5000 | 500 | 服务内部异常 | 否 | 拿 request_id 联系技术支持 |
十、代码示例
cURL
curl -X POST 'https://nowater.lsqykj.com/api/v2/parse' \
-H 'X-AppId: ak_1aea664195' \
-H "X-Timestamp: $(date +%s)" \
-H 'X-Nonce: 8f3a9c2e1b7d' \
-H 'X-Sign: <计算出的签名>' \
-H 'Content-Type: application/json' \
-d '{"url":"https://v.douyin.com/bU_ILkJDCNw/"}'
PHP
<?php
$appid = 'ak_1aea664195';
$secret = '你的secret';
$body = json_encode(['url' => 'https://v.douyin.com/bU_ILkJDCNw/'], JSON_UNESCAPED_SLASHES);
$ts = time();
$nonce = bin2hex(random_bytes(8));
$sign = hash_hmac('sha256', $appid . "\n" . $ts . "\n" . $nonce . "\n" . md5($body), $secret);
$ch = curl_init('https://nowater.lsqykj.com/api/v2/parse');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => $body,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTPHEADER => [
'Content-Type: application/json',
'X-AppId: ' . $appid,
'X-Timestamp: ' . $ts,
'X-Nonce: ' . $nonce,
'X-Sign: ' . $sign,
],
]);
$resp = curl_exec($ch);
curl_close($ch);
$data = json_decode($resp, true);
if ($data['code'] === 0) {
echo $data['data']['videos'][0] ?? '';
} else {
echo '失败: ' . $data['msg'] . ' (' . $data['request_id'] . ')';
}
Node.js
const crypto = require('crypto');
const APPID = 'ak_1aea664195';
const SECRET = '你的secret';
async function parse(url) {
const body = JSON.stringify({ url });
const ts = Math.floor(Date.now() / 1000).toString();
const nonce = crypto.randomBytes(8).toString('hex');
const md5 = crypto.createHash('md5').update(body).digest('hex');
const sign = crypto.createHmac('sha256', SECRET)
.update(`${APPID}\n${ts}\n${nonce}\n${md5}`)
.digest('hex');
const res = await fetch('https://nowater.lsqykj.com/api/v2/parse', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-AppId': APPID,
'X-Timestamp': ts,
'X-Nonce': nonce,
'X-Sign': sign
},
body
});
return res.json();
}
Python
import hashlib, hmac, json, time, secrets, requests
APPID = "ak_1aea664195"
SECRET = "你的secret"
def parse(url: str) -> dict:
body = json.dumps({"url": url}, separators=(",", ":"))
ts = str(int(time.time()))
nonce = secrets.token_hex(8)
plain = f"{APPID}\n{ts}\n{nonce}\n{hashlib.md5(body.encode()).hexdigest()}"
sign = hmac.new(SECRET.encode(), plain.encode(), hashlib.sha256).hexdigest()
r = requests.post(
"https://nowater.lsqykj.com/api/v2/parse",
data=body.encode(),
headers={
"Content-Type": "application/json",
"X-AppId": APPID,
"X-Timestamp": ts,
"X-Nonce": nonce,
"X-Sign": sign,
},
timeout=30,
)
return r.json()
json.dumps 默认带空格。签名和发送必须用同一个 body 字符串——
上面用 separators=(",", ":") 固定格式,并把同一个 body 用于签名和发送。Java
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.util.HexFormat;
public class DspClient {
static String hmacHex(String data, String key) throws Exception {
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(new SecretKeySpec(key.getBytes(StandardCharsets.UTF_8), "HmacSHA256"));
return HexFormat.of().formatHex(mac.doFinal(data.getBytes(StandardCharsets.UTF_8)));
}
static String md5Hex(String s) throws Exception {
MessageDigest md = MessageDigest.getInstance("MD5");
return HexFormat.of().formatHex(md.digest(s.getBytes(StandardCharsets.UTF_8)));
}
public static void main(String[] args) throws Exception {
String appid = "ak_1aea664195", secret = "你的secret";
String body = "{\"url\":\"https://v.douyin.com/bU_ILkJDCNw/\"}";
String ts = String.valueOf(System.currentTimeMillis() / 1000);
String nonce = java.util.UUID.randomUUID().toString().replace("-", "").substring(0, 16);
String plain = appid + "\n" + ts + "\n" + nonce + "\n" + md5Hex(body);
String sign = hmacHex(plain, secret);
// 用 HttpClient 带上 X-AppId / X-Timestamp / X-Nonce / X-Sign 发出
}
}
Go
appid, secret := "ak_1aea664195", "你的secret"
body := []byte(`{"url":"https://v.douyin.com/bU_ILkJDCNw/"}`)
ts := strconv.FormatInt(time.Now().Unix(), 10)
nonce := "8f3a9c2e1b7d"
sum := md5.Sum(body)
plain := appid + "\n" + ts + "\n" + nonce + "\n" + hex.EncodeToString(sum[:])
h := hmac.New(sha256.New, []byte(secret))
h.Write([]byte(plain))
sign := hex.EncodeToString(h.Sum(nil))
req, _ := http.NewRequest("POST", "https://nowater.lsqykj.com/api/v2/parse", bytes.NewReader(body))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("X-AppId", appid)
req.Header.Set("X-Timestamp", ts)
req.Header.Set("X-Nonce", nonce)
req.Header.Set("X-Sign", sign)
resp, err := (&http.Client{Timeout: 30 * time.Second}).Do(req)
十一、常见问题
返回 3001 上游解析失败,要不要重试?
可以重试 1~2 次,中间隔 1 秒。失败不计费,但频繁失败通常是作品已删除或平台限流。
同一条链接连续调用两次,为什么第二次更快?
30 分钟内命中缓存,直接返回上次结果(cache_hit=1),不请求上游。缓存命中仍计 1 次。
videos 数组有多个是什么情况?
不同清晰度。数组按无水印最高清在前,一般取 videos[0] 即可。
图集怎么判断?
看 data.type:video 取 videos,image 取 images。
不要用数组是否为空判断,个别平台会两个都返回。
链接里带一堆分享文案可以吗?
可以,服务端会自动提取第一个 http(s) 链接。
IP 白名单模式下,我加机器后调不通?
新机器的出口 IP 要报备。同一个客户可以申请多个密钥,分别绑不同 IP。
我在自己电脑上直接调 /api/video,返回"IP不在白名单内"?
这是正常的。IP 白名单只放行你报备过的服务器 IP,本地调试的宽带 IP 不在里面。两种办法:
- 把调试机的公网 IP 临时加进白名单(后台 → 密钥 → 编辑 → IP 白名单)
- 改用
/api/v2/parse走签名鉴权,任何 IP 都能调
为什么不用签名也能调 /api/video?
/api/video 是给老客户的兼容接口,沿用原来的 IP 白名单鉴权。
新客户建议直接用 /api/v2/parse,签名鉴权可跨机器、可防重放。
报障要提供什么?
request_id。一条日志能定位到渠道、耗时、扣次和上游原始返回。
附录
A. 平台代码
| code | 平台 | 状态 |
|---|---|---|
douyin | 抖音 | ✅ 可用 |
kuaishou | 快手 | ✅ 可用 |
xhs | 小红书 | ✅ 可用 |
bilibili | B 站 | ✅ 可用 |
weibo | 微博 | ✅ 可用 |
sph | 微信视频号 | ✅ 走 /api/video(传分享链接) |
yt | YouTube | ⏸ 需单独开通 |
twitter | X / Twitter | ⏸ 需单独开通 |
instagram | ⏸ 需单独开通 | |
tiktok | TikTok | ⏸ 需单独开通 |
B. 变更记录
| 日期 | 版本 | 变更 |
|---|---|---|
| 2026-11-05 | v2.0.2 | /api/video 恢复支持微信视频号(传 https://weixin.qq.com/sph/xxx
分享链接即可);/api/sphvideo 仍保持下线 |
| 2026-11-05 | v2.0.1 | /api/sphvideo(微信视频号)已下线,调用返回 {"code":-1,"msg":"视频号接口已下线","data":""} |
| 2026-11-05 | v2.0 | 新增 /api/v2/parse、/api/v2/quota;
/api/video、/api/sphvideo 保持兼容;
改为按次计费,失败自动退款 |
技术支持:报障请提供 request_id + 调用时间 + 平台,可直接定位。