DokumentasiNotifikasi webhook

Notifikasi webhook

Terima notifikasi saat transkripsi selesai atau gagal. Verifikasi tanda tangannya sebelum memproses webhook.

Tambahkan webhook_url saat membuat transkripsi. Saat proses selesai atau gagal, transcript1 mengirim notifikasi ke URL tersebut lewat POST yang dilengkapi tanda tangan.

Gunakan REST API untuk webhook. Tool pada server MCP tidak menerima parameter ini.

Tentukan alamat penerima

Saat mengirim link video yang didukung atau menyiapkan unggahan bertahap lewat POST /v1/transcriptions, sertakan webhook_url pada body JSON. Jika mengunggah file dalam satu request, isi sebagai field form:

{
  "platform": "youtube",
  "url": "https://www.youtube.com/watch?v=dQw4w9WgXcQ",
  "webhook_url": "https://example.com/hooks/transcript1"
}

Pakai URL HTTPS tanpa nama pengguna atau kata sandi. Hasil resolusi URL harus berupa alamat publik. Alamat privat, loopback, link-local, dan alamat nonpublik lain mendapat 422 invalid_request. transcript1 memeriksanya saat transkripsi dibuat dan sebelum setiap pengiriman webhook.

Jangan gunakan webhook_url bersama reuse_recent=true; keduanya tidak bisa digabungkan.

Kapan webhook dikirim

typePemicu
transcription.completedStatus transkripsi berubah menjadi completed.
transcription.failedStatus transkripsi berubah menjadi failed. Baca code dan message di data.error.

Jika transkripsi dibatalkan, tidak ada webhook yang dikirim.

Isi notifikasi

Webhook membawa body JSON berikut:

{
  "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
  }
}

Field created_at mencatat kapan event dibuat. Blok data memuat objek transkripsi, tetapi tanpa transcript; contoh di atas disingkat. Ambil teksnya lewat GET /v1/transcriptions/:id menggunakan id dari data.

Setiap request memakai Content-Type: application/json dan User-Agent bernilai transcript1-webhooks/1.0. Ada juga header tanda tangan untuk verifikasi berikut.

Periksa tanda tangan sebelum membaca JSON

Baca tanda tangan dari header X-Transcript1-Signature pada setiap pengiriman:

X-Transcript1-Signature: t=<unix seconds>,v1=<hex digest>
  • t mencatat waktu penandatanganan request sebagai Unix time dalam detik.
  • v1 berisi HMAC-SHA256 dalam format heksadesimal. Hitung dari string "<t>.<raw body>": gabungkan nilai t, titik, lalu body request persis seperti yang diterima. Gunakan signing secret lengkap sebagai kunci, termasuk awalan whsec_.

Verifikasi byte body asli sebelum melakukan parse JSON. Parse lalu serialisasi ulang bisa mengubah spasi atau urutan key, sehingga tanda tangan tidak lagi cocok. Bandingkan tanda tangan dengan metode constant-time. Tolak request jika nilai t terlalu jauh dari waktu sekarang; contoh berikut memakai toleransi 5 menit.

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));
};

Di Express, baca body sebagai teks mentah agar JSON belum diparse saat verifikasi:

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)

Di Flask, ambil body melalui request.get_data(as_text=True) dan teruskan sebagai raw_body.

Kelola signing secret tim

Signing secret berawalan whsec_ berlaku untuk seluruh tim. Semua webhook dari transkripsi tim memakai secret yang sama. Untuk melihat atau menggantinya, buka halaman “Webhooks” di /dashboard/webhooks. Penggantian berlaku saat itu juga. Semua webhook yang dikirim setelahnya memakai secret baru.

Respons, retry, dan notifikasi ganda

  • Balas dengan status 2xx agar pengiriman dianggap berhasil.
  • Kode status lain, error koneksi, atau tidak ada respons dalam 10 detik dihitung sebagai percobaan gagal.
  • Jika gagal, transcript1 mengirim ulang dengan jeda yang makin panjang (exponential backoff), maksimal 6 percobaan termasuk pengiriman pertama. Jedanya 2, 4, 8, 16, dan 32 menit. Percobaan terakhir berlangsung sekitar satu jam setelah yang pertama.
  • Berikan URL tujuan akhir. transcript1 tidak mengikuti redirect; respons 3xx dihitung gagal.
  • Notifikasi yang sama bisa masuk lebih dari sekali, misalnya karena server Anda baru merespons setelah timeout. Periksa kombinasi data.id dan type untuk mengabaikan duplikat.

Pembaruan terakhir 14 Oktober 2026