DocumentaciónWebhooks

Webhooks

Recibe un aviso firmado cuando la transcripción termina o falla. Aprende a comprobar la firma y manejar los reintentos.

Incluye webhook_url al crear la transcripción. Cuando termine o falle, transcript1 enviará un POST firmado a esa dirección.

Esta opción solo está en la API REST. El servidor MCP no acepta un parámetro para webhooks.

Configura dónde recibir el aviso

Si envías un enlace de video admitido o reservas una subida por partes, agrega webhook_url al JSON de POST /v1/transcriptions. Si subes el archivo completo en un request, agrégalo como campo del formulario:

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

Usa una URL HTTPS que resuelva a una dirección pública, sin usuario ni contraseña en la URL. Las direcciones privadas, loopback, link-local y cualquier otra dirección no pública devuelven 422 invalid_request. La API comprueba la dirección al crear la transcripción y antes de cada envío.

No combines webhook_url con reuse_recent=true.

Eventos

typeCuándo lo recibes
transcription.completedCuando la transcripción pasa a completed.
transcription.failedCuando pasa a failed. Revisa code y message en data.error.

Si cancelas la transcripción, no se envía un webhook.

Qué contiene el request

El aviso llega con un cuerpo 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
  }
}

En created_at tienes la fecha y hora de creación del evento. data contiene el objeto de transcripción, pero no incluye transcript; el ejemplo está abreviado. Para obtener el texto, toma el id de data y consulta GET /v1/transcriptions/:id.

Los headers incluyen Content-Type: application/json, User-Agent con el valor transcript1-webhooks/1.0 y la firma que se explica en la siguiente sección.

Comprueba la firma antes de leer el JSON

Cada envío lleva el header X-Transcript1-Signature:

X-Transcript1-Signature: t=<unix seconds>,v1=<hex digest>
  • t indica cuándo se firmó el request, en segundos Unix.
  • v1 contiene el HMAC-SHA256 en hexadecimal de "<t>.<raw body>". Para calcularlo, concatena el valor de t, un punto y el cuerpo exacto que recibiste. Usa tu secreto de firma completo como clave, sin quitar el prefijo whsec_.

Comprueba la firma con los bytes originales del cuerpo, antes de parsear el JSON. Si lo parseas y vuelves a serializar, los espacios o el orden de las claves pueden cambiar y la firma dejará de coincidir. Haz la comparación en tiempo constante y rechaza los requests cuyo t se aleje demasiado de la hora actual. Los ejemplos aceptan una diferencia de hasta 5 minutos.

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

En Express, recibe el cuerpo como texto sin parsearlo primero:

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)

En Flask, obtén el cuerpo con request.get_data(as_text=True) y pásalo a raw_body.

Dónde consultar o rotar el secreto

El secreto empieza con whsec_ y es compartido por todo el equipo: firma los webhooks de todas sus transcripciones. Puedes verlo o rotarlo en “Webhooks”, dentro de /dashboard/webhooks. Si lo rotas, el cambio es inmediato. Todos los envíos posteriores usan el secreto nuevo.

Respuestas, reintentos y duplicados

  • Responde con cualquier código 2xx para confirmar la recepción.
  • Otro código HTTP, un error de conexión o más de 10 segundos sin respuesta cuentan como un intento fallido.
  • Si falla el envío, transcript1 vuelve a intentarlo hasta completar 6 intentos en total. Espera 2, 4, 8, 16 y 32 minutos entre intentos; el último ocurre aproximadamente una hora después del primero.
  • Configura la URL final. transcript1 no sigue redirecciones: un 3xx también cuenta como fallo.
  • Puedes recibir el mismo aviso más de una vez, por ejemplo si tu servidor responde después del timeout. Usa la combinación de data.id y type para detectar e ignorar duplicados.

Última actualización 14 de octubre de 2026