文档Webhook回调
Webhook回调
配置转录完成或失败的回调通知,验证请求签名,并处理重试和重复通知。
创建转录时填写webhook_url。任务完成或失败后,transcript1会向这个地址发送带签名的POST请求。
只有REST API支持Webhook,MCP服务器不提供这个参数。
配置回调地址
提交视频链接或创建分块上传时,将webhook_url放进POST /v1/transcriptions的JSON请求体。如果是直接上传文件,则将它作为表单字段发送:
{
"platform": "youtube",
"url": "https://www.youtube.com/watch?v=dQw4w9WgXcQ",
"webhook_url": "https://example.com/hooks/transcript1"
}回调地址必须使用HTTPS,不能带用户名或密码,且域名必须解析到公网地址。解析到私有地址、环回地址、链路本地地址或其他非公网地址时,接口会返回422 invalid_request。创建任务和每次发送回调时,系统都会检查地址。
设置了webhook_url,就不能同时使用reuse_recent=true。
哪些状态会触发回调
type | 触发条件 |
|---|---|
transcription.completed | 转录成功,任务状态变为completed。 |
transcription.failed | 转录失败,任务状态变为failed。错误码和说明分别在data.error的code、message中。 |
取消任务不会触发Webhook。
回调请求的内容
回调会发送如下JSON请求体,示例省略了部分字段:
{
"type": "transcription.completed",
"created_at": "2026-10-13T09:30:04.000Z",
"data": {
"id": "tr_8fK2Lm0PqRsTuVwXyZaBcD",
"object": "transcription",
"status": "completed",
"transcript_source": "youtube_captions",
"billed_minutes": 4,
"error": null
}
}created_at记录事件创建时间。data包含转录对象,但不含transcript。需要正文时,取出data中的id,再调用GET /v1/transcriptions/:id获取转录文本。
请求头包括Content-Type: application/json、User-Agent: transcript1-webhooks/1.0,以及用于验签的请求头。
先验签,再处理通知
每次回调都会携带X-Transcript1-Signature,格式如下:
X-Transcript1-Signature: t=<unix seconds>,v1=<hex digest>t是签名时的Unix时间戳,单位为秒。v1是HMAC-SHA256摘要,使用十六进制编码。待签名字符串为"<t>.<raw body>",即t的值、英文句点和原始请求体,依次拼接。计算时使用完整签名密钥,不要去掉whsec_前缀。
必须用收到的原始请求体字节验签,通过后再解析JSON。先解析再序列化,可能改变空白字符或字段顺序,导致验签失败。比较摘要时使用恒定时间比较,同时检查t与当前时间的差值,拒绝时间相差过大的请求。以下示例允许5分钟偏差。
Node.js
import { Buffer } from 'node:buffer';
import crypto from 'node:crypto';
// header: X-Transcript1-Signature, rawBody: the request body exactly as received,
// secret: your full signing secret including the whsec_ prefix.
export const verifySignature = (secret, header, rawBody, nowSeconds = Math.floor(Date.now() / 1000)) => {
const fields = Object.fromEntries(header.split(',').map(part => part.trim().split('=')));
const t = Number(fields.t);
if (!Number.isInteger(t) || !fields.v1 || Math.abs(nowSeconds - t) > 300) {
return false;
}
const expected = crypto.createHmac('sha256', secret).update(`${t}.${rawBody}`).digest('hex');
return fields.v1.length === expected.length
&& crypto.timingSafeEqual(Buffer.from(fields.v1), Buffer.from(expected));
};Express中先将请求体作为原始文本读取,不要提前解析JSON:
import process from 'node:process';
import express from 'express';
import { verifySignature } from './verify-signature.js';
const app = express();
app.post('/hooks/transcript1', express.text({ type: 'application/json' }), (request, response) => {
const header = request.get('X-Transcript1-Signature') ?? '';
if (!verifySignature(process.env.TRANSCRIPT1_WEBHOOK_SECRET, header, request.body)) {
response.sendStatus(400);
return;
}
const { type, data } = JSON.parse(request.body);
console.log(type, data.id, data.status);
response.sendStatus(200);
});Python
import hashlib
import hmac
import time
def verify_signature(secret, header, raw_body, now=None):
# header: the X-Transcript1-Signature header, raw_body: the request body exactly as received,
# secret: your full signing secret including the whsec_ prefix.
fields = dict(part.strip().split("=", 1) for part in header.split(",") if "=" in part)
try:
t = int(fields["t"])
except (KeyError, ValueError):
return False
now = int(time.time()) if now is None else now
if abs(now - t) > 300:
return False
expected = hmac.new(secret.encode(), f"{t}.{raw_body}".encode(), hashlib.sha256).hexdigest()
return hmac.compare_digest(fields.get("v1", ""), expected)Flask中用request.get_data(as_text=True)读取原文,再作为raw_body传入。
查看和轮换签名密钥
签名密钥以whsec_开头,团队内所有转录任务的Webhook共用这个密钥。进入控制台的“Webhooks”页面(/dashboard/webhooks)可以查看或轮换密钥。轮换会立即生效,之后发送的回调都使用新密钥签名。
回调重试和去重
- 接口返回任意
2xx状态码,即视为回调成功。 - 返回其他状态码、连接失败或10秒内未响应,都算发送失败。
- 发送失败后会重试,含首次发送在内最多6次。重试采用指数退避,间隔依次为2、4、8、16和32分钟;最后一次约在首次发送后1小时。
- transcript1不跟随重定向,
3xx也算失败。请直接配置最终接收地址。 - 同一通知可能收到多次,例如服务器已经处理,但响应晚于超时时间。用
data.id和type组合去重,跳过已处理的通知。
最后更新 2026年10月14日