Raphael Serafim· Publicado el 10 de septiembre de 2026· 9 min de lectura

Webhook en producción: firma, reentrega e idempotencia (lo que nadie prueba antes)

Tu webhook funciona en la prueba y falla en producción. Los tres problemas que solo aparecen con volumen: mensaje procesado dos veces, evento fuera de orden y endpoint abierto a cualquiera. Con código para cada uno.

Ver como Markdown

Todo webhook funciona en la primera prueba. Envías un mensaje, el evento llega, el código responde. Lo que falla es el webhook en producción, con volumen — y falla de tres formas que la prueba manual nunca muestra.

Si tu problema es que el evento no llega, el camino es otro: las 7 causas y cómo probarlo. Acá el evento llega. El problema es lo que pasa después.

Problema 1 — El mismo evento procesado dos veces

Por qué pasa

La plataforma reenvía cuando no recibe 200 rápido. Eso es correcto: sin reentrega, un evento se perdería cada vez que tu servidor se reinicia. El comportamiento existe para que no pierdas mensajes.

El efecto colateral es que el mismo evento puede llegar dos o tres veces. Y entonces:

  • el bot responde lo mismo tres veces;
  • el CRM crea tres registros del mismo contacto;
  • el cobro se emite dos veces.

La corrección, en dos capas

Capa 1 — responder 200 antes de procesar. Resuelve la mayoría de los casos en el origen:

javascript
app.post('/webhook/wame', (req, res) => {
  res.sendStatus(200);
  cola.add(req.body).catch(console.error);
});

Capa 2 — idempotencia. La capa 1 reduce el duplicado; no lo elimina. La red falla, y una respuesta puede perderse en el camino después de que tu servidor ya procesó. La garantía real es tratar el messageId como clave única:

javascript
async function procesar(evento) {
  const msg = evento?.entry?.[0]?.changes?.[0]?.value?.messages?.[0];
  if (!msg) return;

  // SETNX con expiración: escribe solo si todavía no existe.
  // La operación es atómica, así que dos entregas simultáneas no
  // pasan las dos — que es el caso que un "if (existe)" seguido
  // de "escribe" deja escapar.
  const inedito = await redis.set(`wh:${msg.id}`, '1', {
    NX: true,
    EX: 60 * 60 * 24,
  });
  if (!inedito) return;   // ya procesamos este evento

  await responderCliente(msg);
}

Sin Redis, la misma idea con una tabla y una restricción de unicidad en message_id funciona igual: intenta insertar, y si viola la restricción, ignora el evento.

El orden importa. Guarda la marca antes de actuar, no después. Marcarla después deja la ventana abierta justo en el intervalo en que ocurre el procesamiento, que es cuando suele llegar la reentrega.

Problema 2 — Endpoint abierto a cualquiera

Si tu URL de webhook es https://api.tuempresa.com/webhook/whatsapp, cualquiera que la descubra puede publicar un JSON falso. Según lo que haga tu handler, eso es un mensaje enviado en nombre de tu cliente o un registro falso en la base.

Capa 1: ruta secreta

El mínimo aceptable, y lleva un minuto:

https://api.tuempresa.com/webhook/wame/a8f3d92e4b17c05f

El secreto está en la ruta. No es criptografía, pero elimina al escáner automático — y es infinitamente mejor que /webhook.

Capa 2: validar la firma

Cuando la plataforma firma el cuerpo, verifica la firma. Y verifícala bien:

javascript
import crypto from 'node:crypto';

function firmaValida(req) {
  const recibida = req.get('X-Hub-Signature-256');
  if (!recibida) return false;

  const esperada = 'sha256=' + crypto
    .createHmac('sha256', process.env.WEBHOOK_SECRET)
    // El cuerpo CRUDO, no el objeto reserializado: JSON.stringify
    // puede reordenar claves y cambiar espaciado, y el hash nunca coincide.
    .update(req.rawBody)
    .digest('hex');

  // Comparación en tiempo constante: `===` filtra información por el
  // tiempo de respuesta y permite descubrir la firma byte a byte.
  const a = Buffer.from(recibida);
  const b = Buffer.from(esperada);
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}

Para tener req.rawBody en Express:

javascript
app.use(express.json({
  verify: (req, _res, buf) => { req.rawBody = buf; },
}));

Capa 3: un secreto por cliente

Si entregas sistemas a varios clientes, no uses el mismo secreto para todos. Un secreto por instancia, guardado junto al registro del cliente: si se filtra uno, rotas ese, no los treinta.

Problema 3 — Eventos fuera de orden

No hay garantía de orden. Dos mensajes enviados en secuencia pueden llegar invertidos, y una reentrega puede poner un evento viejo después de uno nuevo.

Eso rompe una lógica del tipo "el último mensaje define el estado de la conversación":

javascript
// frágil: depende del orden de llegada
conversacion.ultimoMensaje = msg.text.body;

// robusto: el evento más nuevo gana, llegue cuando llegue
if (!conversacion.ultimoTs || msg.timestamp > conversacion.ultimoTs) {
  conversacion.ultimoMensaje = msg.text.body;
  conversacion.ultimoTs = msg.timestamp;
}

Si tu máquina de estados depende de la secuencia, ordena por timestamp del evento. Nunca por el orden en que tu servidor lo recibió.

La arquitectura que resuelve los tres de una vez

javascript
app.post('/webhook/wame/:secreto', async (req, res) => {
  // 1. autenticación, antes de cualquier trabajo
  if (req.params.secreto !== process.env.WEBHOOK_PATH_SECRET) {
    return res.sendStatus(404);   // 404 y no 403: no confirmes que existe
  }
  if (!firmaValida(req)) return res.sendStatus(401);

  // 2. confirma al instante
  res.sendStatus(200);

  // 3. encola; el procesamiento ocurre fuera de la petición
  await cola.add('webhook', req.body, {
    // el propio id del evento como clave: la cola descarta el
    // duplicado antes incluso de que el worker despierte
    jobId: req.body?.entry?.[0]?.changes?.[0]?.value?.messages?.[0]?.id,
  });
});

Con BullMQ, el jobId ya da la idempotencia gratis — un job con id repetido se descarta.

El checklist antes de publicar

  • Responde 200 antes de procesar
  • messageId guardado como clave única, antes de actuar
  • Ruta del webhook con secreto
  • Firma validada sobre el cuerpo crudo, con comparación en tiempo constante
  • Secreto por instancia, no global
  • Estado resuelto por timestamp, no por orden de llegada
  • Estado de entrega tratado aparte del mensaje recibido
  • Un error en el procesamiento no tumba la respuesta del endpoint

Por qué esto es más simple con un solo webhook

Cada punto de arriba es trabajo por integración. Con tres canales en tres plataformas distintas, es tres veces todo: tres validaciones de firma, tres formatos de id, tres lugares donde equivocarse.

Como los tres canales llegan en el mismo sobre, el checklist se escribe una vez y vale para los tres — el campo provider dice el canal y nada más cambia.

¿Listo para automatizar tu WhatsApp?

Crea tu cuenta gratis y empieza a enviar mensajes por la API en minutos.

Empezar gratis

Preguntas frecuentes

¿Por qué el mismo webhook llega dos veces?+

Porque tu endpoint no confirmó el recibo a tiempo. Toda plataforma de webhooks reenvía cuando no recibe un 200 rápido, y reenviar es el comportamiento correcto: es lo que garantiza que un evento no se pierda si tu servidor se cae. Lo que te toca a ti es hacer el procesamiento idempotente.

¿Qué es la idempotencia en un webhook?+

Es la propiedad de procesar el mismo evento varias veces con el mismo resultado final. En la práctica: guarda el identificador del mensaje antes de actuar e ignora el evento si ya fue visto. Sin eso, una reentrega se convierte en cobro doble, respuesta duplicada o dos registros en el CRM.

¿Cómo protejo el endpoint del webhook?+

Tres capas, de la más simple a la más fuerte: un token secreto en la ruta de la URL, validación de firma sobre el cuerpo de la petición cuando la plataforma envía una, y restricción por origen. El mínimo aceptable es la URL secreta; un endpoint con ruta previsible y sin verificación acepta eventos falsos de cualquiera.

¿Debo procesar el webhook de forma asíncrona?+

Sí, siempre que el procesamiento pase de unos milisegundos. Responde 200 de inmediato, mete el evento en una cola y procésalo fuera del ciclo de la petición. Eso resuelve la reentrega por timeout en el origen y además hace que tu endpoint sobreviva a los picos.

¿Los eventos llegan en orden?+

No hay garantía. Dos mensajes enviados en secuencia pueden llegar en orden inverso, y un estado de entrega puede llegar antes que el propio mensaje en escenarios de reentrega. Si el orden importa para tu lógica, ordena por el timestamp del evento, no por el orden de llegada.

Sigue leyendo