Sistema de Webhooks

El sistema de webhooks permite recibir una notificación automática cuando todos los clips de un video han sido procesados y el JSON final está listo.

Flujo Completo

1. POST /api/videos con webhookUrl
       ↓
2. Clips se encolan en RabbitMQ
       ↓
3. Workers procesan clips (TTS, etc)
       ↓
4. Último clip se procesa → Video READY
       ↓
5. Se envía POST al webhookUrl con JSON final
       ↓
6. Tu servicio recibe el JSON y renderiza el video

Uso Básico

1. Enviar video con webhook

curl -X POST http://localhost:5173/api/videos \
  -H "Authorization: Bearer tu-api-key" \
  -H "Content-Type: application/json" \
  -d '{
    "title": "Mi Video",
    "webhookUrl": "https://tu-servidor.com/webhook/video-ready",
    "output": {
      "fps": 30,
      "width": 1920,
      "height": 1080
    },
    "timeline": [
      {
        "asset": { "type": "tts", "text": "Hola mundo" },
        "start": 0,
        "length": "{{audio.duration}}"
      }
    ]
  }'

2. Recibir notificación

Cuando todos los clips estén procesados, recibirás un POST en tu webhookUrl:

{
  "event": "video.ready",
  "timestamp": "2025-09-30T10:30:00.000Z",
  "video": {
    "id": "cmg6...",
    "title": "Mi Video",
    "status": "READY",
    "duration": 5.5,
    "createdAt": "2025-09-30T10:29:00.000Z",
    "processedAt": "2025-09-30T10:30:00.000Z"
  },
  "json": {
    "output": {
      "fps": 30,
      "width": 1920,
      "height": 1080,
      "format": "mp4",
      "codec": "h264",
      "durationInFrames": 165
    },
    "timeline": [
      {
        "asset": {
          "type": "audio",
          "src": "public/assets/tts/.../clip-0-xxx.mp3",
          "volume": 1
        },
        "start": 0,
        "length": 2.5
      }
    ]
  },
  "stats": {
    "totalClips": 3,
    "readyClips": 3,
    "failedClips": 0
  }
}

Headers del Webhook

El webhook incluye estos headers útiles:

Content-Type: application/json
User-Agent: Videomaker-Webhook/1.0
X-Video-Id: cmg6...
X-Event: video.ready

Respuesta Esperada

Tu endpoint debe responder con:

  • Status 200-299: Webhook procesado correctamente
  • Status 4xx/5xx: Error (se registra pero NO se reintenta automáticamente)

Ejemplo de respuesta:

{
  "success": true,
  "message": "Video encolado para renderizado",
  "renderId": "render-123"
}

Timeout y Reintentos

  • Timeout: 30 segundos
  • Reintentos automáticos: NO (debes reenviar manualmente si falla)

Si el webhook falla, puedes reenviarlo manualmente desde el admin o mediante API.


Testing Local

Opción 1: Endpoint de prueba incluido

Usa el endpoint de testing que viene incluido:

curl -X POST http://localhost:5173/api/videos \
  -H "Authorization: Bearer tu-api-key" \
  -d '{
    "webhookUrl": "http://localhost:5173/api/webhook-test",
    "output": {...},
    "timeline": [...]
  }'

El endpoint /api/webhook-test imprimirá el webhook recibido en los logs.

Opción 2: Usar ngrok

# Terminal 1: Servidor
npm run dev

# Terminal 2: ngrok
ngrok http 5173

# Usar URL de ngrok como webhookUrl
curl -X POST http://localhost:5173/api/videos \
  -H "Authorization: Bearer tu-api-key" \
  -d '{
    "webhookUrl": "https://abc123.ngrok.io/api/webhook-test",
    ...
  }'

Opción 3: Webhook.site

Usa https://webhook.site para inspeccionar webhooks:

curl -X POST http://localhost:5173/api/videos \
  -H "Authorization: Bearer tu-api-key" \
  -d '{
    "webhookUrl": "https://webhook.site/tu-unique-url",
    ...
  }'

Implementación del Webhook Receiver

Ejemplo en Node.js/Express

app.post('/webhook/video-ready', async (req, res) => {
  try {
    const { event, video, json, stats } = req.body;

    if (event !== 'video.ready') {
      return res.status(400).json({ error: 'Unknown event' });
    }

    console.log(`Video ${video.id} is ready with ${stats.totalClips} clips`);

    // Renderizar video con Remotion o ffmpeg
    await renderVideo(json);

    res.json({ success: true, message: 'Video enqueued for rendering' });
  } catch (error) {
    console.error('Webhook error:', error);
    res.status(500).json({ error: error.message });
  }
});

Ejemplo en Python/Flask

@app.route('/webhook/video-ready', methods=['POST'])
def webhook_video_ready():
    data = request.json

    if data['event'] != 'video.ready':
        return {'error': 'Unknown event'}, 400

    video_id = data['video']['id']
    final_json = data['json']

    print(f"Video {video_id} is ready")

    # Renderizar video
    render_video(final_json)

    return {'success': True, 'message': 'Video enqueued'}

Seguridad

1. Verificar IP de origen (opcional)

const ALLOWED_IPS = ['1.2.3.4', '5.6.7.8'];

app.post('/webhook/video-ready', (req, res) => {
  const clientIp = req.ip;

  if (!ALLOWED_IPS.includes(clientIp)) {
    return res.status(403).json({ error: 'Forbidden' });
  }

  // Procesar webhook...
});

2. Verificar signature con HMAC (futuro)

const crypto = require('crypto');

const signature = req.headers['x-signature'];
const payload = JSON.stringify(req.body);
const expectedSignature = crypto
  .createHmac('sha256', process.env.WEBHOOK_SECRET)
  .update(payload)
  .digest('hex');

if (signature !== expectedSignature) {
  return res.status(401).json({ error: 'Invalid signature' });
}

Monitoreo

Ver webhooks enviados

En el admin panel:

  • /admin/videos/:id muestra si el webhook se envió
  • Campo webhookSent: true si se envió
  • Campo webhookSentAt: Timestamp del envío
  • Campo webhookResponse: Respuesta del servidor (status + body)

Logs

# Ver logs de workers
pm2 logs clip-worker

# Buscar webhooks enviados
pm2 logs clip-worker | grep "Sending webhook"

Reenviar Webhook Manualmente

Si el webhook falla y necesitas reenviarlo:

Opción 1: Reset desde base de datos

UPDATE video_configurations
SET webhookSent = false, webhookSentAt = NULL
WHERE id = 'cmg6...';

Luego el próximo clip que se procese reenviará el webhook.

Opción 2: Llamar al servicio directamente

import { webhookService } from '~/services/webhook.server';

await webhookService.retryWebhook('cmg6...');

Troubleshooting

Webhook no se envía

Verificar:

  1. ¿El video está en estado READY?

    SELECT status FROM video_configurations WHERE id = 'cmg6...';
    
  2. ¿Hay webhookUrl configurado?

    SELECT webhookUrl FROM video_configurations WHERE id = 'cmg6...';
    
  3. ¿Ya se envió?

    SELECT webhookSent, webhookSentAt FROM video_configurations WHERE id = 'cmg6...';
    

Webhook falla con timeout

  • Aumentar timeout en app/services/webhook.server.ts:
    const timeoutId = setTimeout(() => controller.abort(), 60000); // 60 segundos
    

Webhook recibe datos corruptos

  • Verificar que tu endpoint acepta Content-Type: application/json
  • Verificar que tu endpoint no tiene middleware que modifica el body

Payload Completo del Webhook

interface WebhookPayload {
  event: 'video.ready';
  timestamp: string; // ISO 8601

  video: {
    id: string;
    title: string;
    description: string | null;
    status: 'READY';
    duration: number | null;
    createdAt: string;
    processedAt: string;
  };

  json: {
    output: {
      fps: number;
      format: string;
      width: number;
      height: number;
      codec: string;
      durationInFrames: number;
    };
    timeline: Array<{
      asset: {...};
      start: number;
      length: number;
      // ... otros campos
    }>;
  };

  stats: {
    totalClips: number;
    readyClips: number;
    failedClips: number;
  };
}

Ejemplos de Uso

Caso 1: Renderizado con Remotion

app.post('/webhook/video-ready', async (req, res) => {
  const { video, json } = req.body;

  // Guardar JSON en S3 o filesystem
  await fs.writeFile(`/tmp/${video.id}.json`, JSON.stringify(json));

  // Encolar renderizado con Remotion
  await renderQueue.add('render-video', {
    videoId: video.id,
    compositionId: 'MyComposition',
    inputProps: json
  });

  res.json({ success: true });
});

Caso 2: Renderizado con ffmpeg

app.post('/webhook/video-ready', async (req, res) => {
  const { video, json } = req.body;

  // Convertir JSON a comandos ffmpeg
  const ffmpegCommand = buildFFmpegCommand(json);

  // Renderizar
  await exec(ffmpegCommand);

  res.json({ success: true });
});

Caso 3: Notificación a usuario

app.post('/webhook/video-ready', async (req, res) => {
  const { video } = req.body;

  // Notificar usuario por email/slack
  await sendEmail({
    to: video.userEmail,
    subject: `Video "${video.title}" está listo`,
    body: `Tu video ha sido procesado. Descarga el JSON aquí: ${video.jsonUrl}`
  });

  res.json({ success: true });
});

Best Practices

  1. Responde rápido: No hagas procesamiento pesado en el webhook. Encola el trabajo y responde con 200 OK.

  2. Idempotencia: El webhook puede enviarse múltiples veces (retry manual). Guarda el video.id para evitar duplicados.

  3. Logging: Registra todos los webhooks recibidos para debugging.

  4. Timeout corto: No dejes tu webhook colgado. Responde en <5 segundos.

  5. Retry logic: Si fallas procesando el webhook, reintenta TÚ llamando al API para obtener el JSON.


Próximas Mejoras (Roadmap)

  • HMAC signature para seguridad
  • Reintentos automáticos con backoff exponencial
  • Webhooks para eventos adicionales (video.failed, clip.processed, etc.)
  • Dead Letter Queue para webhooks que fallan múltiples veces
  • Dashboard para ver historial de webhooks

¡Listo! Ahora puedes recibir notificaciones automáticas cuando tus videos estén procesados. 🎉