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:truesi 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:
¿El video está en estado
READY?SELECT status FROM video_configurations WHERE id = 'cmg6...';¿Hay
webhookUrlconfigurado?SELECT webhookUrl FROM video_configurations WHERE id = 'cmg6...';¿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
Responde rápido: No hagas procesamiento pesado en el webhook. Encola el trabajo y responde con 200 OK.
Idempotencia: El webhook puede enviarse múltiples veces (retry manual). Guarda el
video.idpara evitar duplicados.Logging: Registra todos los webhooks recibidos para debugging.
Timeout corto: No dejes tu webhook colgado. Responde en <5 segundos.
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. 🎉