En esta sección
Guías / Recibir eventos firmados en tu backend

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.

Actualizada el Ver .md
StackNode · Express · SDK de TypeScript
Endpointswebhooks.subscriptions.create · webhooks.constructEvent · subscriptions.test

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