Recibir eventos firmados en tu backend
Un endpoint que recibe los eventos de tu workspace, verifica la firma y sobrevive a los reintentos sin procesar nada dos veces.
Sondear la API para saber si algo pasó es caro y llega tarde. Con una suscripción, Meteor te hace POST cuando el hecho ocurre. Esta receta arma el endpoint completo: firma verificada, respuesta rápida y un procesamiento que aguanta reintentos.
Antes de empezar
Tu key necesita webhooks:manage, más el scope del evento que quieras escuchar — runs:read para run.completed, contacts:read para contact.created, y así. Si pides un evento cuyo scope no tienes, no llega.
1. Suscríbete
import Met from '@meteor.ia/sdk';
const met = new Met(process.env.MET_API_KEY!, {
workspaceId: Number(process.env.MET_WORKSPACE_ID),
});
const sub = await met.webhooks.subscriptions.create({
url: 'https://tu-servidor.com/webhooks/met',
enabled_events: ['run.completed', 'run.failed', 'task.completed'],
description: 'Procesador de resultados',
});
console.log(sub.secret); // whsec_… — se muestra UNA sola vez
enabled_events: ['*'] te trae todos. Conviene enumerar los que de verdad procesas: un endpoint que recibe diecisiete tipos y actúa sobre tres es un endpoint que se cae por un evento que nadie miró.
Los tipos disponibles salen de la API, así que no hay que adivinarlos:
const tipos = await met.webhooks.subscriptions.eventTypes();
2. El endpoint
import express from 'express';
const app = express();
// Cuerpo CRUDO. La firma se calcula sobre los bytes exactos que mandó Meteor:
// si tu framework parsea y vuelve a serializar el JSON, deja de coincidir.
app.use('/webhooks/met', express.raw({ type: 'application/json' }));
app.post('/webhooks/met', async (req, res) => {
let evento;
try {
evento = met.webhooks.constructEvent(
req.body,
req.headers['x-met-signature'] as string,
process.env.MET_WEBHOOK_SECRET!,
);
} catch {
// Firma inválida o timestamp viejo. No reintentar: no va a mejorar.
return res.status(400).send('firma inválida');
}
// 2xx primero, trabajo después: Meteor considera fallida la entrega que no
// responde, y tu procesamiento puede tardar más que su tiempo de espera.
res.sendStatus(200);
await encolar(evento);
});
app.listen(3000);
constructEvent verifica la firma en tiempo constante y chequea el timestamp del header X-Met-Signature: t=<ts>,v1=<hmac>, que es lo que impide que alguien te reenvíe una entrega vieja legítima.
3. Sobrevivir a los reintentos
Si tu endpoint no responde 2xx, Meteor reintenta con espera creciente —1 min, 5 min, 30 min, 2 h, 6 h, 24 h— hasta seis veces. Eso significa que el mismo evento puede llegarte más de una vez, y también que puede llegar tarde y desordenado.
Guarda el id del evento antes de actuar:
async function encolar(evento) {
const nuevo = await tuBase.insertarSiNoExiste('eventos_met', { id: evento.id });
if (!nuevo) return; // ya lo procesamos: la entrega es un reintento
await procesar(evento);
}
La regla que evita el susto: el orden de llegada no es el orden de los hechos. Si tu lógica depende de la secuencia, ordénala por el created del evento —un epoch en segundos— y no por cuándo lo recibiste.
Cada entrega viene con esta forma:
{
"id": "evt_9f2c…",
"object": "event",
"type": "run.completed",
"created": 1786886400,
"livemode": true,
"data": { }
}
4. Probarlo antes de que pase algo de verdad
await met.webhooks.subscriptions.test(sub.id); // entrega de prueba, firmada igual
Y contra tráfico real, sin desplegar:
met listen --forward http://localhost:3000/webhooks/met --types run.completed,run.failed
Si una entrega falló y ya arreglaste la causa, se puede reintentar a mano desde el log de entregas, que está en tu panel en Desarrolladores → Actividad.
Y después
- El catálogo completo de eventos y el detalle de los reintentos: Webhooks.
- Si lo que quieres es reaccionar en el mismo segundo y no en la próxima entrega: Eventos en vivo.