En esta sección
Guías / Un agente que contesta WhatsApp

Un agente que contesta WhatsApp

De un mensaje que entra por WhatsApp a una respuesta del Met, con tu lógica en el medio y el traspaso a una persona cuando hace falta.

Actualizada el Ver .md
StackNode · Express · SDK de TypeScript
Endpointscontact.message.received · runs.create · contacts.sendMessage · contacts.setAutopilot

Con el autopiloto encendido, Meteor ya contesta los mensajes de WhatsApp solo. Esta receta es para lo otro: cuando quieres tu propia lógica en el medio — consultar tu inventario antes de responder, escalar según el cliente, escribir en tu CRM, decidir cuándo calla el Met y contesta una persona.

Antes de empezar

Conecta el canal de WhatsApp a tu workspace desde el panel (Canales → WhatsApp). La API no conecta canales: eso pasa una vez y necesita la aprobación de Meta.

Tu key necesita cinco scopes, uno por cada cosa que hace la receta:

scopepara qué
webhooks:manageregistrar el endpoint que recibe los eventos
conversations:readrecibir contact.message.received
runs:executeejecutar el Met
channels:sendresponderle al contacto
handoff:manageapagar y prender el autopiloto

1. Suscríbete a los mensajes que entran

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: ['contact.message.received'],
});

console.log(sub.secret);   // whsec_… — se muestra UNA sola vez

Guarda el secret apenas lo recibes: lo necesitas para verificar cada entrega y no se vuelve a mostrar.

2. Recibe el mensaje y contesta

El evento llega con este cuerpo:

{
  "contact_id": 4821,
  "message_id": 99312,
  "channel_id": 12,
  "channel_type": "whatsapp",
  "content": "¿Todavía tienen la bici azul?",
  "attachments": []
}
import express from 'express';

const app = express();
// El cuerpo CRUDO es obligatorio: si tu framework re-serializa el JSON antes de
// verificar, la firma deja de coincidir aunque el contenido sea idéntico.
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 {
    return res.status(400).send('firma inválida');
  }

  // Responde YA. Meteor reintenta si no ve un 2xx, y un Met puede tardar varios
  // segundos: contestar después de pensar te duplica los mensajes.
  res.sendStatus(200);

  if (evento.type !== 'contact.message.received') return;

  const { contact_id, content } = evento.data;
  const run = await met.runs.create({ input: content });
  await met.contacts.sendMessage(contact_id, run.output);
});

app.listen(3000);

El run que creas por API no queda atado al contacto: recibe el texto que le pasas y nada más. Si quieres que el Met responda sabiendo con quién habla, arma tú el input — por ejemplo con met.contacts.retrieve(contact_id) y met.contacts.messages(contact_id) — o usa conversation_id para que la conversación tenga memoria entre runs.

3. Pásasela a una persona cuando haga falta

Un Met que no sabe algo tiene que poder soltar la conversación. Con el autopiloto apagado los mensajes siguen llegando a tu bandeja, pero el Met deja de responder a ese contacto hasta que lo vuelvas a prender.

if (/hablar con (alguien|una persona|un asesor)/i.test(content)) {
  await met.contacts.setAutopilot(contact_id, false);
  await met.contacts.sendMessage(contact_id, 'Te comunico con alguien del equipo 👋');
  await met.contacts.createNote(contact_id, 'Pidió atención humana.');
  return;
}

Para devolverle el control al Met: met.contacts.setAutopilot(contact_id, true).

La ventana de 24 horas

Esto no es un límite de Meteor y no se puede saltar: WhatsApp solo te deja escribir libremente durante las 24 horas siguientes al último mensaje de la persona. Pasada esa ventana, lo único que entra es una plantilla aprobada por Meta:

await met.contacts.sendTemplate(contact_id, {
  name: 'recordatorio_cita',
  language: 'es',
  namespace: process.env.WA_TEMPLATE_NAMESPACE!,   // obligatorio
  params: { '1': 'Ana', '2': 'martes a las 3' },
});

Si tu flujo contesta a destiempo —una cola, un reintento nocturno— esa ventana es la primera causa a mirar, antes que la key o los scopes.

Probarlo sin desplegar nada

El CLI te reenvía los eventos reales del workspace a tu máquina, así que puedes escribir el handler contra tráfico de verdad antes de tener servidor:

met listen --forward http://localhost:3000/webhooks/met --types contact.message.received

Y después