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.completedJob chuyển sang completed.
transcription.failedJob 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>
  • t ghi 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 3xx vẫ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.id và type để nhận ra và bỏ qua bản trùng.

Cập nhật lần cuối 14 tháng 10, 2026