DokuWebhooks einrichten
Webhooks einrichten
Empfange signierte Webhooks für abgeschlossene oder fehlgeschlagene Transkriptionen und prüfe ihre Signatur.
Hinterlege beim Anlegen einer Transkription eine webhook_url. Sobald die Transkription fertig ist oder fehlschlägt, sendet transcript1 einen signierten POST an diese URL.
Webhooks kannst du nur über die REST-API einrichten. Beim MCP-Server gibt es dafür keinen Parameter.
Wohin soll der Webhook gehen?
Für einen unterstützten Video-Link oder einen Upload in Chunks setzt du webhook_url im JSON-Body von POST /v1/transcriptions. Beim Chunk-Upload machst du das schon beim Anlegen, bevor du die Datei sendest. Lädt dein Request die ganze Datei auf einmal hoch, gibst du webhook_url als Formularfeld mit.
{
"platform": "youtube",
"url": "https://www.youtube.com/watch?v=dQw4w9WgXcQ",
"webhook_url": "https://example.com/hooks/transcript1"
}Verwende eine HTTPS-URL ohne Benutzername oder Passwort. Der Host muss auf eine öffentliche IP-Adresse auflösen. Private Adressen, Loopback, Link-Local und andere nicht öffentliche Adressen lehnt die API mit 422 invalid_request ab. Diese Prüfung läuft beim Anlegen der Transkription und erneut bei jeder Zustellung.
Wenn du webhook_url angibst, darfst du nicht gleichzeitig reuse_recent=true setzen.
Bei diesen Events kommt ein Webhook
type | Auslöser |
|---|---|
transcription.completed | Die Transkription ist fertig und hat den Status completed. |
transcription.failed | Die Transkription ist mit Status failed fehlgeschlagen. Den Fehler findest du als code und message in data.error. |
Brichst du eine Transkription ab, sendet transcript1 keinen Webhook.
So sieht der Webhook-Body aus
Der Webhook enthält einen JSON-Body:
{
"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
}
}Unter created_at steht, wann das Event entstanden ist. In data steckt das Transkriptionsobjekt, aber ohne transcript; das Beispiel oben zeigt nur einen Ausschnitt. Hole den Text separat mit GET /v1/transcriptions/:id ab. Die passende id findest du in data.
Außerdem sendet transcript1 die Header Content-Type: application/json, User-Agent mit dem Wert transcript1-webhooks/1.0 und den folgenden Signatur-Header.
Prüfe die Signatur vor dem Verarbeiten
Im Header X-Transcript1-Signature stehen bei jeder Zustellung diese Werte:
X-Transcript1-Signature: t=<unix seconds>,v1=<hex digest>tgibt als Unix-Zeit in Sekunden an, wann transcript1 den Request signiert hat.- Für
v1wird der HMAC-SHA256 von"<t>.<raw body>"hexadezimal kodiert: erst der Wert vont, dann ein Punkt, dann der unveränderte Request-Body. Der HMAC-Schlüssel ist dein vollständiges Signing Secret mit dem Präfixwhsec_.
Berechne die Signatur aus den empfangenen Bytes, bevor du JSON parst. Wenn du den Body erst parst und dann neu serialisierst, können sich Leerzeichen oder die Reihenfolge der Schlüssel ändern. Die Signatur passt dann nicht mehr.
Vergleiche die Signaturen in konstanter Zeit. Prüfe auch t und lehne Requests ab, deren Zeitstempel zu weit von der aktuellen Zeit abweicht. Die folgenden Beispiele tolerieren 5 Minuten.
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));
};Lass Express den Body als Text einlesen. So kannst du die Signatur prüfen, bevor JSON geparst wird:
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)In Flask holst du den unveränderten Body mit request.get_data(as_text=True) und übergibst ihn als raw_body.
Signing Secret anzeigen oder wechseln
Dein Signing Secret beginnt mit whsec_ und gilt für das gesamte Team. Damit signiert transcript1 die Webhooks aller Transkriptionen dieses Teams.
Unter /dashboard/webhooks kannst du das Secret auf der Webhooks-Seite anzeigen oder durch ein neues ersetzen. Der Wechsel gilt sofort. Alle danach gesendeten Webhooks tragen eine Signatur mit dem neuen Secret.
Zustellung, Wiederholungen und Duplikate
- Antworte mit einem
2xx-Status, um den Empfang zu bestätigen. - Ein anderer Statuscode oder ein Verbindungsfehler zählt als fehlgeschlagener Versuch. Dasselbe gilt, wenn dein Server nicht innerhalb von 10 Sekunden antwortet.
- Schlägt die Zustellung fehl, versucht transcript1 es erneut. Insgesamt gibt es höchstens 6 Versuche. Die Abstände wachsen exponentiell: 2, 4, 8, 16 und 32 Minuten. Der letzte Versuch liegt damit etwa eine Stunde nach dem ersten.
- Gib direkt die endgültige URL an. transcript1 folgt keinen Weiterleitungen und wertet auch
3xxals fehlgeschlagenen Versuch. - Rechne mit mehrfachen Zustellungen, etwa wenn dein Server erst nach dem Timeout antwortet. Erkenne Duplikate an der Kombination aus
data.idundtypeund ignoriere sie.
Zuletzt aktualisiert 14. Oktober 2026