Webhooks entrantes
Registra endpoints que disparan tareas o emiten eventos, con firma HMAC.
Un webhook entrante es un endpoint que Meteor expone para que un tercero le haga POST. Cuando llega un evento, Meteor puede disparar una tarea o emitir un evento interno. Necesita el scope webhooks:manage.
Estos son webhooks entrantes (tercero → Meteor). Si lo que quieres es que Meteor te avise cuando pasa algo (
run.completed, etc.), salta a Eventos salientes más abajo.
Crear un webhook
const wh = await met.webhooks.create({
name: 'Entrada de leads',
binding_type: 'generic', // 'task' ejecuta una tarea; 'generic' solo emite evento
verifier_type: 'hmac_sha256', // 'none' para sin firma
});
console.log(wh.url); // la URL a la que apunta el tercero
console.log(wh.secret); // ⚠️ se muestra UNA sola vez
Guarda el secret apenas lo recibes: no se vuelve a mostrar. Con él verificas que cada entrega vino realmente de tu integración.
Verificar la firma
El tercero firma el cuerpo con el secret (HMAC-SHA256) y manda la firma en un header. Del lado de Meteor, la verificación es automática si el webhook es hmac_sha256.
Rotar el secret
Si sospechas que se filtró:
const rotated = await met.webhooks.rotateSecret(wh.id);
console.log(rotated.secret); // nuevo secret; invalida el anterior
Ver entregas
const events = await met.webhooks.events(wh.id, { limit: 20 });
const sample = await met.webhooks.sample(wh.id); // último payload recibido
sample es útil para configurar el mapeo de campos del payload.
Conectar un flujo
Un webhook generic puede disparar un flujo cuando recibe un evento (connectFlow en la API). Así, un POST externo arranca una automatización completa en Meteor.
Eventos salientes (Meteor → tu servidor)
Un evento saliente es Meteor haciéndote POST a *ti* cuando algo pasa en tu workspace — por ejemplo, cuando un Run termina. Registras un endpoint, eliges qué eventos quieres, y Meteor entrega cada uno firmado con HMAC para que puedas verificar que vino de Meteor.
Registrar un endpoint
Registras el endpoint desde tu panel: Desarrolladores → Actividad → Webhooks. Eliges la URL de tu servidor y qué eventos quieres (o * para todos). Al crearlo, Meteor te muestra el secreto de firma (whsec_...) una sola vez — guárdalo, lo necesitas para verificar cada entrega.
Cambiar los eventos de un endpoint
Para sumar o quitar eventos, cambiar la URL o la descripción, usa PATCH /webhook-endpoints/{id}. El secreto de firma no cambia: tu verificación sigue funcionando igual, sin redesplegar nada. enabled_events reemplaza la lista completa, así que para sumar un evento manda la lista actual más el nuevo:
const [ep] = await met.webhooks.subscriptions.list();
await met.webhooks.subscriptions.update(ep.id, {
enabled_events: [...ep.enabled_events, 'contact.message.failed'],
});
Si te suscribiste con *, no tienes que hacer nada: los eventos nuevos del catálogo te llegan solos.
Catálogo de eventos
Son 30. La columna scope es el permiso que la key debe portar para recibir ese evento: el mismo con el que leerías ese recurso por la API.
| Evento | Cuándo dispara | Scope |
|---|---|---|
run.completed | Un Run terminó con éxito. | runs:read |
run.failed | Un Run falló. | runs:read |
channel.run.completed | El Met respondió un mensaje de canal. | runs:read |
channel.run.failed | El Met falló al responder un mensaje de canal. | runs:read |
contact.created | Se creó un contacto. | contacts:read |
contact.updated | Se actualizó un contacto. | contacts:read |
contact.message.received | Entró un mensaje de un contacto por un canal (WhatsApp, etc.). | conversations:read |
conversation.handoff | Una conversación pasó a operador humano. | conversations:read |
task.completed | Deprecado: no se emite desde el 2026-08-31, cuando se retiró la ejecución de tareas. Sale del catálogo el 2027-04-07. | tasks:read |
billing.threshold | El gasto de Energía de una API key cruzó un umbral de su presupuesto (50/80/100%). | billing:read |
app.authorized | Un workspace autorizó tu app OAuth (nuevo grant). | integrations:read |
app.revoked | Un workspace revocó el acceso de tu app OAuth (grant revocado). | integrations:read |
snapshot.published | Una plantilla propia fue aprobada y publicada al marketplace. | snapshots:read |
snapshot.install.completed | La instalación de una plantilla terminó. | snapshots:read |
snapshot.install.failed | La instalación de una plantilla falló. | snapshots:read |
conversion.sent | Una conversión se emitió con éxito a Meta (CAPI). | conversions:read |
conversion.discarded | Una conversión propuesta fue descartada por la compuerta (incluye la razón). | conversions:read |
automation.run.failed | Una automatización falló al ejecutarse. | automations:read |
contact.message.sent | Salió un mensaje hacia un contacto, lo haya escrito el Met o una persona. | conversations:read |
contact.message.failed | Un mensaje hacia un contacto no se pudo entregar. Trae el código de error del proveedor. | conversations:read |
contact.deleted | Se borró un contacto. | contacts:read |
broadcast.completed | Una difusión terminó de enviarse (trae cuántos salieron y cuántos fallaron). | channels:read |
channel.template.approved | Meta aprobó una plantilla de WhatsApp. | channels:read |
channel.template.rejected | Meta rechazó una plantilla de WhatsApp (trae el motivo). | channels:read |
channel.template.status_updated | Una plantilla de WhatsApp cambió de estado por otra razón (pausada, deshabilitada, en apelación…): trae el estado de Meta. | channels:read |
call.missed | Una llamada quedó sin contestar. | conversations:read |
billing.energy.low | El saldo de Energía cruzó hacia abajo el umbral bajo. Dispara en el cruce, no en cada ejecución. | billing:read |
billing.energy.depleted | El saldo de Energía se agotó. | billing:read |
billing.energy.recharged | Entró una recarga de Energía (compra, recarga automática o abono del partner). | billing:read |
billing.subscription.deactivated | La suscripción del workspace quedó desactivada. | billing:read |
* no es un evento: es el comodín «todos los del catálogo», y cubre también los que agreguemos después. Si prefieres enterarte de un evento nuevo antes de empezar a recibirlo, suscríbete a la lista explícita.
Cinco cosas que ahorran una depuración:
task.completedestá deprecado y no se emite. Lo disparaba la ejecución de tareas, que se retiró el 31 de agosto de 2026. Sigue en el catálogo y enenabled_eventshasta el 7 de abril de 2027 para que ningún endpoint que lo tenga en su lista falle, pero no vas a recibir ninguno: sácalo cuando toques tu endpoint. Ver el changelog.channel.run.*no esrun.*. Unrun.*es un Run de la API, conidconsultable; unchannel.run.*es el Met respondiendo un mensaje de canal (WhatsApp y demás) y no deja fila en Runs. Si escuchas solorun.completed, las respuestas de canal no te llegan nunca.app.authorizedyapp.revokedse entregan al workspace que es dueño de la app OAuth —el tuyo, el del developer—, no al que autoriza.- Las entregas de prueba llegan con
type: "ping", que no está en el catálogo. Tu handler debería ignorar lo que no reconoce en vez de fallar. - Una conversación completa son dos eventos.
contact.message.receivedtrae lo que escribe el contacto; lo que le responden —el Met, tu equipo desde la bandeja o el dueño desde WhatsApp Business en su celular— llega encontact.message.sent. Si te suscribes solo al primero, nunca ves las respuestas.contact.message.senttraesent_by(metuoperator) ysent_from:met,phone(el celular del dueño),flow(un flujo o automatización) ometeor(cualquier otra salida desde Met: bandeja, difusión, plantilla o API). Para distinguir con más detalle, usaorigin:panel(una persona desde la app),api(tu integración con API key o SDK),mcp,flow,automation,broadcast,met,phoneosystem. Es el mismo campo que trae cada mensaje enGET /contacts/{id}/messages. Las notas internas de la conversación —cambios de asignación, de estado, etiquetas— no salen como mensaje: no le llegan al contacto. Los dos traenattachmentscon la misma forma (url,type,name) y, en las notas de voz,transcriptcon el texto: las del contacto y también las que graba tu equipo. - Un mensaje que no llegó avisa aparte.
contact.message.failedsale cuando un mensaje hacia un contacto queda sin entregar: el proveedor lo rechazó al enviarlo, o WhatsApp lo marcó como fallido después, casi siempre a los pocos minutos. Trae los mismos campos quecontact.message.sent, máserror—concode, el código de WhatsApp cuando lo hay, ymessage— ytemplate_namesi era una plantilla. El código separa causas que piden cosas distintas:131026, el número no puede recibir el mensaje (revísalo);131049, Meta frenó las plantillas de marketing a esa persona (el número está bien: espacia los envíos);132012, un parámetro no coincide con la plantilla (corrige el envío). Sale una sola vez por mensaje. El mismo dato queda enmetadata.delivery_statusymetadata.delivery_errorde cada mensaje enGET /contacts/{id}/messages.
Esta misma tabla la devuelve la API, siempre al día:
const { data } = await met.webhooks.subscriptions.eventTypes();
// [{ type: 'run.completed', description: '…', scope: 'runs:read' }, …]
Verificar la firma
Cada entrega trae el header X-Met-Signature: t=<ts>,v1=<hmac>. Nunca proceses un evento sin verificarlo — usa el helper del SDK, que hace la verificación timing-safe y chequea el timestamp (anti-replay):
import Met from '@meteor.ia/sdk';
const met = new Met(process.env.MET_API_KEY!);
app.post('/webhooks/met', (req, res) => {
let event;
try {
event = met.webhooks.constructEvent(req.rawBody, req.headers['x-met-signature'], WHSEC);
} catch {
return res.status(400).send('firma inválida');
}
if (event.type === 'run.completed') { /* ... */ }
res.sendStatus(200); // responde 2xx rápido; Meteor reintenta si no.
});
Necesitas el cuerpo crudo (raw body) para verificar la firma — configura tu framework para no re-serializar el JSON antes de
constructEvent.
Reintentos
Si tu endpoint no responde 2xx en 15 segundos, Meteor reintenta con espera creciente: el primer intento y luego a 1 min → 5 min → 30 min → 2 h → 6 h, 6 intentos en total (unas 8,6 horas desde el primero). Después marca la entrega como failed; la puedes volver a mandar con POST /webhook-endpoints/{id}/deliveries/{deliveryId}/retry o POST /events/{id}/replay. Cada reintento lleva el mismo id de evento, también en el header X-Met-Event-Id: úsalo para no procesar dos veces lo mismo. Las entregas salen en paralelo, así que el orden no está garantizado: ordena por created. El log de intentos por endpoint está en tu Workbench (sección Desarrolladores).