Tài liệuWebhook
Webhook
Nhận thông báo khi job hoàn tất hoặc thất bại. Kiểm tra chữ ký webhook để xác nhận request đến từ transcript1.
Muốn nhận thông báo thay vì liên tục kiểm tra trạng thái, thêm webhook_url lúc tạo job. Khi job hoàn tất hoặc thất bại, transcript1 gửi đến đó một request POST có chữ ký.
Bạn chỉ dùng được webhook qua REST API. Các tool của MCP server không nhận tham số này.
Khai báo URL nhận webhook
Khi gọi POST /v1/transcriptions bằng link video hoặc tạo phiên tải lên theo chunk, đặt webhook_url trong body JSON. Nếu gửi file bằng một request, đặt trong trường form. Ví dụ với link video:
{
"platform": "youtube",
"url": "https://www.youtube.com/watch?v=dQw4w9WgXcQ",
"webhook_url": "https://example.com/hooks/transcript1"
}Chọn URL HTTPS không kèm tên đăng nhập hoặc mật khẩu, phân giải ra địa chỉ IP công khai. Địa chỉ private, loopback, link-local và các địa chỉ không công khai khác sẽ nhận 422 invalid_request. transcript1 kiểm tra địa chỉ lúc tạo job và trước mỗi lần gửi webhook.
Không gửi webhook_url cùng reuse_recent=true.
Khi nào nhận được webhook?
type | Điều kiện gửi |
|---|---|
transcription.completed | Job chuyển sang completed. |
transcription.failed | Job chuyển sang failed. Đọc code và message trong data.error để biết lỗi. |
Nếu job bị hủy, transcript1 không gửi webhook.
Nội dung request
Bạn nhận body JSON theo dạng sau:
{
"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
}
}Dùng created_at để biết thời điểm tạo sự kiện. data chứa object transcription nhưng không kèm transcript; ví dụ trên đã lược bớt trường. Lấy id từ data, rồi gọi GET /v1/transcriptions/:id để đọc transcript.
Các header gồm Content-Type: application/json, User-Agent: transcript1-webhooks/1.0 và header chữ ký ở phần dưới.
Kiểm tra chữ ký webhook
Đọc chữ ký từ header X-Transcript1-Signature của mỗi request:
X-Transcript1-Signature: t=<unix seconds>,v1=<hex digest>tghi thời điểm ký request theo Unix time, tính bằng giây.- Để tính
v1, ghép giá trịt, dấu chấm và body nguyên gốc thành chuỗi"<t>.<raw body>". Tính HMAC-SHA256 bằng toàn bộ secret ký webhook, giữ cả tiền tốwhsec_, rồi chuyển digest sang hex.
Kiểm tra chữ ký trên đúng các byte body nhận được, trước khi parse JSON. Nếu parse rồi serialize lại, khoảng trắng hay thứ tự key có thể đổi và chữ ký sẽ không khớp. So sánh bằng hàm constant-time. Đồng thời kiểm tra t để từ chối request quá lệch so với giờ hiện tại; các ví dụ dưới đây chấp nhận độ lệch tối đa 5 phút.
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));
};Nếu dùng Express, đọc body dưới dạng raw text, không parse JSON trước bước kiểm tra:
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)Nếu dùng Flask, lấy body bằng request.get_data(as_text=True) và truyền vào raw_body.
Secret dùng để ký webhook
Mỗi team dùng chung một secret có tiền tố whsec_ để ký webhook cho mọi job của team. Bạn có thể xem hoặc đổi secret ở “Webhooks”, đường dẫn /dashboard/webhooks. Secret mới có hiệu lực ngay khi đổi. Mọi webhook gửi từ thời điểm đó đều dùng secret mới.
Gửi lại và xử lý trùng lặp
- Trả mã
2xxđể xác nhận đã nhận webhook. - Mã khác, lỗi kết nối hoặc không phản hồi trong 10 giây đều tính là gửi thất bại.
- Nếu gửi thất bại, transcript1 thử lại với thời gian chờ tăng theo cấp số nhân, tối đa 6 lần tính cả lần đầu. Các khoảng chờ là 2, 4, 8, 16 và 32 phút; lần cuối cách lần đầu khoảng một giờ.
- Cung cấp URL đích cuối cùng. transcript1 không theo redirect, nên response
3xxvẫn tính là thất bại. - Cùng sự kiện có thể đến nhiều lần, chẳng hạn khi server trả lời sau timeout. Dùng cặp
data.idvàtypeđể nhận ra và bỏ qua bản trùng.
Cập nhật lần cuối 14 tháng 10, 2026