Tienes un servicio que debe reaccionar cuando algo sucede en otro lugar. Un pago se liquida. Un registro de cliente cambia. Un repo recibe un push. Podrías hacer polling a un API cada minuto y desperdiciar ciclos preguntando "¿hay algo nuevo?" una y otra vez, o puedes dejar que el sistema de origen te llame cuando ocurra el evento.
Ahí es donde terminan la mayoría de los artículos con ejemplos de webhooks. Muestran una ruta, imprimen el body JSON, devuelven 200, y lo dan por terminado. Esa versión funciona justo hasta que alguien envía una petición falsificada, hace replay de una válida, o tu handler falla porque el framework parseó el body antes de verificar la firma.
Esta guía sigue el camino que usarás en producción. Los ejemplos son lo bastante pequeños para copiarlos, pero incluyen las partes que importan: manejo del raw body, verificación HMAC, validación de timestamps, ack rápido y debugging práctico.
Tabla de contenidos
- Qué son los webhooks y por qué usarlos
- Anatomía de una petición HTTP de webhook
- Cómo verificar firmas de webhooks de forma segura
- Protección contra ataques de replay
- Cómo crear un Webhook Receiver en Node.js
- Cómo construir un receptor de webhook en Python
- Construir un receptor de webhooks en Go
- Técnicas esenciales de depuración
- Checklist para webhooks listos para producción
- Preguntas frecuentes sobre webhooks
Qué son los webhooks y por qué usarlos
Tu proveedor de facturación marca una factura como pagada a las 02:13. Si tu app se entera a las 02:14, el cliente obtiene acceso de inmediato. Si se entera en el siguiente ciclo de polling, espera, soporte recibe un ticket y tus logs se llenan de ruido evitable. Los webhooks resuelven ese problema de sincronización enviando un callback HTTP cuando ocurre el evento.
En términos prácticos, un webhook es un POST orientado a eventos de un sistema a otro. El proveedor detecta un cambio, como invoice.paid, order.created, o push, y envía los datos del evento a una URL que tú controlas. Eso elimina el bucle constante de «¿algo nuevo?» que crea el polling y reduce muchas peticiones innecesarias.
Este patrón aparece en sistemas reales porque se ajusta bien a eventos de negocio. Stripe publica resultados de pagos. GitHub publica actividad de repositorios. Shopify publica actualizaciones de pedidos. La forma es simple, pero el comportamiento en producción no lo es. Un webhook que actualiza dinero, acceso o inventario merece la misma atención que cualquier endpoint público de API, especialmente cuando entran en juego reintentos, duplicados y tráfico no confiable.
El modelo mental que ayuda
Una forma útil de enmarcar el flujo de un webhook es como cuatro partes que trabajan juntas:
- Sistema de origen. El servicio que detecta el evento.
- Endpoint de destino. Tu ruta HTTP que lo recibe.
- Evento. El cambio con nombre que ocurrió, como
invoice.paidopush. - Payload. El cuerpo de la petición con los detalles que necesita tu code.
El proveedor envía datos sobre algo que ya ocurrió. Tu trabajo es verificar el remitente, confirmar que la petición es reciente y aplicar el cambio una sola vez. Esa última parte importa más de lo que admiten muchos tutoriales básicos. En producción, la entrega duplicada es un comportamiento normal, no un caso aislado.
Regla práctica: Usa webhooks para actualizaciones basadas en eventos. Usa polling para lecturas programadas, backfills o proveedores que no ofrecen eventos salientes.
Para equipos que construyen algo más amplio automatización de workflows e integración de datos, los webhooks suelen convertirse en la capa de eventos que mantiene los sistemas sincronizados sin tráfico de peticiones innecesario. Si trabajas en servicios con mucha integración, Capgo’s artículos de desarrollo backend son un contexto útil, porque los problemas centrales aparecen en torno a reintentos, colas, observabilidad y manejo de fallos.
Qué funciona y qué falla en producción
Las configuraciones que aguantan bien suelen ser aburridas por diseño. Suscríbete solo a los eventos que necesites. Mantén los endpoints acotados por proveedor o familia de eventos. Guarda los IDs de evento para que las entregas duplicadas no repitan efectos secundarios. Devuelve una respuesta 2xx rápida en cuanto la petición esté validada y encolada, y ejecuta después la lógica de negocio más lenta de forma asíncrona.
La versión frágil es fácil de reconocer. Un único endpoint genérico lo gestiona todo. Las comprobaciones de firma se omiten en las pruebas iniciales y nunca se recuperan. El handler escribe directamente en tablas críticas antes de comprobar si el evento es auténtico o está obsoleto. Eso funciona en una demo, pero falla ante tormentas de reintentos, caídas de proveedores o un atacante que reenvía peticiones antiguas.
Ese trade-off define el resto de esta guía. La versión “hello world” de un receptor de webhooks es pequeña. La versión lista para producción añade verificación de firma, defensa contra replay, manejo de duplicados y hooks de depuración desde el inicio.
Anatomía de una petición HTTP de webhook
Antes de escribir code, ayuda ver la solicitud como HTTP crudo en lugar de como un objeto de framework. Un webhook típico es simplemente un POST HTTP a un endpoint público con headers y un cuerpo JSON.
Una solicitud cruda simple
POST /webhooks/orders HTTP/1.1
Host: your-app.example
Content-Type: application/json
User-Agent: Provider-Webhooks/1.0
X-Webhook-Signature: sha256=abc123example
X-Webhook-Timestamp: 1712345678
{
"event": "order.created",
"id": "evt_123",
"data": {
"order_id": "ord_456",
"status": "created"
}
}
Las partes importantes son directas:
- Método. En la práctica, las entregas de webhooks suelen ser solicitudes POST.
- Content-Type. Most modern providers send JSON.
- User-Agent. Útil para depurar, pero nunca suficiente para confiar.
- Cabecera de firma. Contiene la verificación de autenticidad del proveedor.
- Cabecera de timestamp. Se usa para rechazar solicitudes obsoletas o reenviadas.
Por qué importa la forma del body
Tu code normalmente no se preocupa por cada campo. Le importan el tipo de evento, el identificador del evento y el objeto de negocio dentro data. Por eso los buenos handlers solo parsean lo que necesitan y registran el resto para depurar.
OpenAPI modela este patrón directamente. OpenAPI 3.1.0 añadió soporte de primera clase para webhooks con un nivel superior webhooks objeto, donde cada webhook se describe como un Path Item pero es activado por el proveedor. El ejemplo canónico usa un newPet webhook con un post operación, un cuerpo de solicitud JSON y una 200 respuesta para indicar la recepción, como se muestra en el ejemplo de webhook de OpenAPI.
Si estás documentando tus propios contratos de receptor o proveedor, ejemplos sólidos ayudan más que prosa abstracta de esquemas. Me gusta usar referencias como Ejemplos de la documentación de API de SheetMergy porque dejan claro cómo encajan los ejemplos de peticiones, las descripciones de campos y las respuestas esperadas.
Un webhook es simple en la capa de transporte. La mayoría de los fallos vienen de suposiciones erróneas sobre headers, encoding del body o reglas de firma.
Cómo verificar firmas de webhooks de forma segura
Un webhook firmado responde a una pregunta: ¿este payload proviene de alguien que conoce el secreto compartido?
Eso es distinto a verificar si la solicitud es reciente o si ya la procesaste. La verificación de la firma es el primer filtro, no el último.

El flujo de verificación
El flujo habitual de HMAC es el siguiente:
- Lee la firma desde el header del proveedor.
- Lee la body raw de la petición exactamente como se recibió.
- Carga tu secreto de webhook desde la configuración segura.
- Recalcula el HMAC esperado usando el mismo algoritmo.
- Compara la firma recibida y la calculada con una comparación segura en el tiempo.
- Rechaza la solicitud si no coinciden.
El paso del cuerpo crudo es donde fallan muchas implementaciones que de otro modo serían buenas. Si tu framework analiza el JSON primero, reformatea los espacios en blanco o cambia detalles de codificación antes de calcular el hash, tu firma calculada no coincidirá con la del proveedor.
Qué vigilar en code reales
Estos son los errores que más veo:
- Hashear JSON parseado. No lo hagas
JSON.stringify(req.body)y esperar que coincida. - Usar igualdad de cadenas normal. Usa una comparación segura en el tiempo.
- Hardcodear secretos. Guárdalos en variables de entorno o en un secrets manager.
- Confiar solo en los headers. Un header de firma solo tiene sentido si lo verificas.
Para equipos que refuerzan el manejo de secretos entre servicios, la guía de Capgo sobre API: clave de seguridad para cumplir con la App Store es relevante porque se aplica la misma disciplina. La rotación de secretos, el acceso con alcance limitado y evitar fugas en los logs también importan para los receptores de webhooks.
Un ejemplo genérico de verificación
const crypto = require('crypto');
function verifySignature(rawBody, receivedSignature, secret) {
const expected = crypto
.createHmac('sha256', secret)
.update(rawBody)
.digest('hex');
const a = Buffer.from(receivedSignature, 'utf8');
const b = Buffer.from(expected, 'utf8');
if (a.length !== b.length) return false;
return crypto.timingSafeEqual(a, b);
}
Este ejemplo es intencionalmente genérico. Los proveedores reales suelen añadir prefijos a las firmas, incluir timestamps en el contenido firmado o codificar el digest de otra forma. La regla es la misma: sigue el formato de firma exacto del proveedor y verifica siempre contra el payload sin procesar.
Protección contra ataques de replay
Un webhook firmado puede seguir siendo peligroso si llega horas más tarde y tu handler lo trata como nuevo. Esto pasa más a menudo de lo que los equipos esperan. Los proxies registran el tráfico, los payloads de las peticiones acaban en el lugar equivocado, o un proveedor reintenta tras un fallo de red y tu endpoint procesa el mismo evento dos veces.

La verificación de firma responde a una pregunta: ¿el remitente creó este payload con el secreto compartido? La protección contra reenvíos responde a otra: ¿debería aceptarse esta solicitud ahora mismo? Los receptores en producción necesitan ambas.
La comprobación mínima que de verdad importa
Una defensa práctica contra reenvíos comienza con un timestamp firmado. El proveedor incluye un timestamp en las cabeceras o en el mensaje firmado, y tu receptor rechaza las solicitudes que caen fuera de una pequeña ventana de tolerancia.
Ese flujo debería verse así:
- Lee el timestamp desde la ubicación definida por el proveedor. No adivines el nombre de la cabecera.
- Páralo como un entero o una fecha en formato RFC, según la especificación del proveedor.
- Compáralo con el reloj de tu servidor.
- Rechaza las solicitudes que sean demasiado antiguas o demasiado futuras.
- Verifica el timestamp como parte del esquema de firma cuando el proveedor lo soporte.
Ese último punto es importante. Si el timestamp no está cubierto por la firma, un atacante puede insertar un timestamp nuevo y reutilizar el cuerpo original. Siempre verifico el formato exacto de firma del proveedor antes de confiar en la lógica del timestamp.
Qué elegir para la ventana de tolerancia
Cinco minutos es un valor predeterminado común. Es lo suficientemente corto para reducir la ventana de ataque, pero lo suficientemente largo para soportar pequeñas desviaciones de reloj y la latencia de red normal.
Aquí hay un equilibrio. Una ventana de 30 segundos suena más segura, pero falla con más frecuencia en sistemas reales, especialmente cuando entran en juego reintentos, colas o latencia regional. Una ventana de 30 minutos es más fácil de operar, pero le da mucho más tiempo a un atacante si se expone una solicitud firmada. Comienza con unos pocos minutos, sincroniza tus servidores con NTP y luego ajusta solo si el patrón de entrega del proveedor lo permite.
Replay defense is not just a timestamp check
La validación del timestamp bloquea solicitudes obsoletas. No impide el procesamiento duplicado dentro de la ventana válida. Si el mismo evento firmado se entrega dos veces dentro de esa ventana, tu aplicación aún debe reconocerlo.
Usa una segunda capa:
- Registra los IDs de eventos o de entrega en un almacén de vida corta como Redis.
- Trata los handlers como idempotentes para que las entregas repetidas no creen pedidos, correos o acciones de facturación duplicados.
- Registra las solicitudes obsoletas rechazadas con códigos de motivo, pero nunca registres secretos ni payloads sensibles completos.
- Devuelve una respuesta rápida tras la validación y delegar el trabajo pesado a una cola.
Los equipos que ya piensan en ventanas de expiración y revocación reconocerán el patrón. La guía de Capgo sobre patrones de revocación de tokens en apps de Capacitor cubren la misma idea operativa. Una credencial o solicitud que fue válida una vez no debería ser de confianza para siempre.
Firmado y obsoleto sigue siendo inseguro.
Cómo crear un Webhook Receiver en Node.js
Node con Express sigue siendo la forma más rápida de poner en marcha un receiver serio, pero hay una trampa que importa más que cualquier otra. Necesitas acceso al raw body antes de que Express lo convierta en un objeto.

Un ejemplo de Express orientado a producción
const express = require('express');
const crypto = require('crypto');
const app = express();
const PORT = process.env.PORT || 3000;
const WEBHOOK_SECRET = process.env.WEBHOOK_SECRET;
// Capture raw body for signature verification
app.use(
express.json({
verify: (req, res, buf) => {
req.rawBody = buf;
},
})
);
function safeEqual(a, b) {
const aBuf = Buffer.from(a, 'utf8');
const bBuf = Buffer.from(b, 'utf8');
if (aBuf.length !== bBuf.length) return false;
return crypto.timingSafeEqual(aBuf, bBuf);
}
function verifySignature(rawBody, secret, receivedSignature) {
const expected = crypto
.createHmac('sha256', secret)
.update(rawBody)
.digest('hex');
return safeEqual(expected, receivedSignature);
}
function isFresh(timestampHeader, toleranceSeconds = 300) {
const timestamp = Number(timestampHeader);
if (!Number.isFinite(timestamp)) return false;
const now = Math.floor(Date.now() / 1000);
return Math.abs(now - timestamp) <= toleranceSeconds;
}
app.post('/webhooks/example', async (req, res) => {
const signature = req.get('x-webhook-signature');
const timestamp = req.get('x-webhook-timestamp');
if (!WEBHOOK_SECRET) {
return res.status(500).send('Webhook secret is not configured');
}
if (!signature || !timestamp) {
return res.status(400).send('Missing required security headers');
}
if (!isFresh(timestamp)) {
return res.status(401).send('Stale webhook');
}
const valid = verifySignature(req.rawBody, WEBHOOK_SECRET, signature);
if (!valid) {
return res.status(401).send('Invalid signature');
}
// Acknowledge quickly
res.status(200).send('OK');
// Process after acknowledgement
try {
const event = req.body;
console.log('Accepted event:', event.event, event.id);
// enqueueJob(event)
} catch (err) {
console.error('Post-ack processing failed:', err);
}
});
app.listen(PORT, () => {
console.log(`Webhook receiver listening on ${PORT}`);
});
Por qué esta estructura es robusta
Algunas decisiones aquí son deliberadas:
- La captura del body raw ocurre en el middleware. Esto preserva los bytes originales para el hashing.
- El timestamp se valida antes de la lógica de negocio. No tiene sentido procesar tráfico obsoleto.
- La ruta responde
200rápido. Las tareas largas deben ir en una cola o en una tarea en segundo plano. - El procesamiento post-ack está aislado. Aunque falle la lógica downstream, la ruta del receptor se mantiene ligera.
Los secrets son el punto débil en muchas implementaciones de webhooks. No los guardes en el código fuente, no los pegues en fixtures de test y no los imprimas en los logs. Si necesitas un proceso más amplio para la rotación y la gestión en CI, la guía de Capgo sobre gestión de secrets en pipelines de CI/CD cubre bien la parte operativa.
Un recorrido breve ayuda si quieres ver las piezas en movimiento:
Lo que cambiaría para un sistema en vivo
Para una integración real con un proveedor, añadiría deduplicación de IDs de eventos en almacenamiento persistente, logs estructurados con IDs de solicitud y una cola detrás de la ruta de confirmación. También evitaría un único endpoint genérico si varios proveedores usan formatos de firma diferentes. Los manejadores separados son más fáciles de entender y más difíciles de romper.
Cómo construir un receptor de webhook en Python
Flask es una buena opción para un ejemplo de webhook limpio porque el manejo de solicitudes es explícito y la biblioteca estándar de Python ya te da lo que necesitas para HMAC.
Lo principal a recordar es lo mismo que en Node. Verifica contra los bytes crudos de la solicitud, no contra el diccionario JSON parseado.
Un ejemplo de Flask con comprobaciones de firma y timestamp
import os
import time
import hmac
import hashlib
from flask import Flask, request, jsonify
app = Flask(__name__)
WEBHOOK_SECRET = os.environ.get("WEBHOOK_SECRET", "")
def is_fresh(timestamp_header, tolerance_seconds=300):
try:
timestamp = int(timestamp_header)
except (TypeError, ValueError):
return False
now = int(time.time())
return abs(now - timestamp) <= tolerance_seconds
def verify_signature(raw_body, secret, received_signature):
expected = hmac.new(
secret.encode("utf-8"),
raw_body,
hashlib.sha256
).hexdigest()
return hmac.compare_digest(expected, received_signature)
@app.route("/webhooks/example", methods=["POST"])
def webhook():
if not WEBHOOK_SECRET:
return "Webhook secret is not configured", 500
signature = request.headers.get("X-Webhook-Signature")
timestamp = request.headers.get("X-Webhook-Timestamp")
if not signature or not timestamp:
return "Missing required security headers", 400
if not is_fresh(timestamp):
return "Stale webhook", 401
raw_body = request.get_data()
if not verify_signature(raw_body, WEBHOOK_SECRET, signature):
return "Invalid signature", 401
payload = request.get_json(silent=True) or {}
# Acknowledge receipt
response = jsonify({"status": "ok"})
# In production, queue payload here instead of heavy sync work
print("Accepted event:", payload.get("event"), payload.get("id"))
return response, 200
if __name__ == "__main__":
app.run(port=5000, debug=True)
Detalles específicos de Flask que importan
request.get_data() es la llamada clave aquí. Te da los bytes crudos del cuerpo. Si saltas directamente a request.json, ya has cruzado la línea donde las discrepancias de firma se vuelven confusas.
Algunas notas de implementación:
- Usa
hmac.compare_digesten lugar de igualdad simple. - Trata las cabeceras faltantes como un fallo del cliente y recházalo temprano.
- Usa
silent=Truepara el análisis JSON si prefieres controlar el manejo de errores en lugar de dejar que Flask los lance. - Mantén la ruta delgada. Encola el trabajo si el payload activa algo costoso.
No depures desajustes de firma relajando las comprobaciones de seguridad. Depúralos imprimiendo exactamente qué bytes has hasheado y qué formato espera el proveedor.
Dónde suelen atascarse los equipos
El fallo común es probar con un cuerpo JSON manual y luego cambiar a un proveedor real, descubriendo que la firma ya no coincide. Eso suele significar una de tres cosas: el proveedor firma un sobre con marca de tiempo, la firma está codificada de forma distinta a la que asumiste, o el middleware modificó el cuerpo antes de la verificación.
Cuando eso ocurra, deja de cambiar la criptografía code al azar. Captura las cabeceras y el cuerpo en bruto, reproduce el hash en un script aislado y pequeño, y solo entonces devuélvelo a la ruta de Flask.
Construir un receptor de webhooks en Go
Go es una gran elección para receptores de webhooks porque la biblioteca estándar es suficiente. No necesitas un framework para obtener un manejador pequeño y fiable, y el code es fácil de mantener honesto.
Lo único que debes tener en cuenta es el manejo del body. r.Body es un stream. Léelo una vez, hashea los bytes que obtuviste y luego deserializa desde esos mismos bytes.
Un ejemplo con la biblioteca estándar
package main
import (
"crypto/hmac"
"crypto/sha256"
"crypto/subtle"
"encoding/hex"
"encoding/json"
"io"
"log"
"net/http"
"os"
"strconv"
"time"
)
type WebhookPayload struct {
Event string `json:"event"`
ID string `json:"id"`
Data json.RawMessage `json:"data"`
}
func isFresh(timestampHeader string, toleranceSeconds int64) bool {
ts, err := strconv.ParseInt(timestampHeader, 10, 64)
if err != nil {
return false
}
now := time.Now().Unix()
diff := now - ts
if diff < 0 {
diff = -diff
}
return diff <= toleranceSeconds
}
func verifySignature(rawBody []byte, secret string, received string) bool {
mac := hmac.New(sha256.New, []byte(secret))
mac.Write(rawBody)
expected := hex.EncodeToString(mac.Sum(nil))
if len(expected) != len(received) {
return false
}
return subtle.ConstantTimeCompare([]byte(expected), []byte(received)) == 1
}
func webhookHandler(secret string) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
return
}
signature := r.Header.Get("X-Webhook-Signature")
timestamp := r.Header.Get("X-Webhook-Timestamp")
if signature == "" || timestamp == "" {
http.Error(w, "missing required security headers", http.StatusBadRequest)
return
}
if !isFresh(timestamp, 300) {
http.Error(w, "stale webhook", http.StatusUnauthorized)
return
}
rawBody, err := io.ReadAll(r.Body)
if err != nil {
http.Error(w, "failed to read body", http.StatusBadRequest)
return
}
if !verifySignature(rawBody, secret, signature) {
http.Error(w, "invalid signature", http.StatusUnauthorized)
return
}
var payload WebhookPayload
if err := json.Unmarshal(rawBody, &payload); err != nil {
http.Error(w, "invalid json", http.StatusBadRequest)
return
}
w.WriteHeader(http.StatusOK)
w.Write([]byte("OK"))
log.Printf("accepted event=%s id=%s", payload.Event, payload.ID)
}
}
func main() {
secret := os.Getenv("WEBHOOK_SECRET")
if secret == "" {
log.Fatal("WEBHOOK_SECRET is not set")
}
http.HandleFunc("/webhooks/example", webhookHandler(secret))
log.Println("listening on :8080")
log.Fatal(http.ListenAndServe(":8080", nil))
}
Por qué Go resulta sólido aquí
Algunos beneficios destacan:
- El handler es explícito. Sin magia de middleware oculto.
- El tipado ayuda en los bordes. El parsing de headers, la conversión de timestamps y la decodificación JSON fallan de forma clara.
- Los paquetes estándar de crypto son suficientes. Sin dependencias extra para la verificación HMAC básica.
Notas operativas
Si el volumen de webhooks crece, el modelo de concurrencia de Go te da margen para distribuir trabajo en segundo plano sin cambiar tu punto de entrada HTTP. Incluso así, mantén el receptor estrecho. Acepta, valida, confirma y luego delega.
Los mejores handlers de webhooks en Go que he visto son aburridos. No mezclan la verificación de transporte con la lógica de negocio, y no realizan trabajo pesado en la base de datos antes de que la respuesta regrese.
Técnicas esenciales de depuración
Un bug de webhook suele manifestarse como un mensaje de soporte, no como un stack trace. El proveedor afirma que entregó el evento. Tu endpoint dice que nada llegó a la app, o que la verificación de firma falló en una solicitud que parece válida a primera vista. En ese punto, depurar implica reconstruir el intercambio HTTP exacto, byte a byte, y demostrar dónde se rompió.

Un kit de depuración práctico
Empieza con el formato de red.
Si la verificación de firma falla, captura el cuerpo de la solicitud cruda exactamente como se recibió, junto con las cabeceras utilizadas para la verificación. En la práctica, el bug suele ser aburrido. Un framework analizó el JSON antes de hacer el hash, un proxy cambió la codificación, o una repetición de prueba omitió la cabecera de marca de tiempo original. Registrar el objeto analizado no es suficiente. Necesitas los bytes originales y las entradas de verificación.
Estas herramientas ayudan a aislar el problema rápidamente:
- Captura de la petición raw. Registra headers, content type, content length y el body sin modificar durante la investigación.
- Endpoints de inspección de peticiones. Servicios como
webhook.siteayudan a confirmar qué envió el remitente. - Tunneling local.
ngroky herramientas similares te permiten probar contra un receptor local manteniendo al proveedor en el loop. - Replay manual. Reconstruye la petición con
curlo Postman usando el mismo body y headers. Es la forma más rápida de confirmar si el problema es tu code o el payload del proveedor. - Logs de entrega del proveedor. El dashboard del remitente suele incluir códigos de respuesta, historial de reintentos e identificadores de petición para cotejar con tus logs.
El patrón importa. Trabaja de fuera hacia adentro. Primero verifica que el proveedor envió lo que esperabas. Luego verifica que tu servidor recibió los mismos bytes. Después verifica que tu code hasheó los mismos bytes con el mismo secreto y las mismas reglas de timestamp.
Logs que realmente ayudan
Buenos logs de webhook deberían responder tres preguntas en una sola búsqueda:
| Pregunta | Campo de log útil |
|---|---|
| ¿Llegó la solicitud? | route, method, received_at |
| ¿Por qué se rechazó? | missing_header, stale_timestamp, signature_failed |
| ¿Puedo correlacionarlo después? | event_id, provider_request_id |
Un cuarto campo ayuda en sistemas reales. Añade un local request_id generado por tu receptor para poder seguir la solicitud a través de los logs de tu app, cola y workers.
Sé selectivo con lo que guardas. Nunca registres secretos. Evita volcar payloads completos de producción si incluyen datos de clientes, access tokens o detalles de facturación. Un patrón más seguro es registrar metadatos más un hash corto del body. Eso aún te permite comparar reintentos y verificar si dos entregas fueron idénticas.
Reproduce fallos con las entradas originales
Esta es la parte que los tutoriales básicos omiten. Si no puedes reproducir exactamente la solicitud fallida, estás adivinando.
Guarda un webhook fallido como:
- bytes del cuerpo en bruto
- todas las cabeceras relacionadas con la firma
- timestamp de la solicitud
- tipo de contenido
- ID de solicitud del proveedor
Luego, reprodúcela contra un endpoint de staging. Si la reproducción pasa, compara qué cambió en tránsito. Los culpables habituales incluyen middleware que normaliza los cuerpos de solicitud, discrepancias de codificación de caracteres y balanceadores de carga que eliminan o reescriben cabeceras. También he visto fallos causados por equipos que copian payloads desde vistas de dashboard formateadas en lugar del cuerpo de solicitud real. La diferencia de espacios en blanco por sí sola fue suficiente para romper la verificación HMAC.
Para un diagnóstico más amplio de releases y transporte móvil, la misma disciplina de depuración aparece en la guía de Capgo sobre herramientas para depurar actualizaciones OTA en Capacitor. Diferente transporte, misma lección. Captura la ruta de solicitud real antes de modificar la aplicación code.
Si la verificación de firma falla, inspecciona los bytes crudos, las cabeceras exactas usadas en la verificación y el valor de la marca de tiempo antes de tocar la criptografía code.
Checklist para webhooks listos para producción
Un manejador de webhooks suele verse bien en staging justo hasta la primera tormenta de reintentos, un payload malformado o una discrepancia de firma a las 2 a. m. El estándar de producción es más alto. El receptor debe rechazar solicitudes falsificadas, aceptar reintentos legítimos y dar a los operadores suficiente señal para depurar fallos sin exponer datos sensibles.
Comprobaciones de seguridad y corrección
- Verifica la firma de cada solicitud. Las URLs de los endpoints se filtran. Las URLs de prueba se comparten en chats. La verificación de firmas es el control que te indica que el remitente conocía el secreto compartido.
- Rechaza solicitudes antiguas. Una firma válida en un payload antiguo aún puede reutilizarse. Aplica una tolerancia de timestamp que coincida con el modelo de reintentos del proveedor.
- Hashea el cuerpo crudo, no el JSON parseado. El middleware puede reordenar claves, normalizar espacios en blanco o cambiar la codificación. La verificación debe ejecutarse sobre los bytes exactos que llegaron.
- Mantén los secretos de firma fuera de code. Las variables de entorno son la base. Un gestor de secretos es más adecuado si rotas credenciales con frecuencia o operas en múltiples entornos.
- Falla de forma cerrada ante errores de autenticación. Si la cabecera de firma falta, está malformada o usa un esquema inesperado, rechaza la solicitud y registra la razón.
Comprobaciones de fiabilidad
- Confirma rápidamente. Los proveedores suelen tratar cualquier 2xx como éxito, así que valida la solicitud, persiste lo que necesites y delega el trabajo lento en una cola o worker.
- Haz que los handlers sean idempotentes. El mismo evento puede llegar más de una vez. Clave los efectos secundarios por ID de evento, ID de entrega u otro identificador estable del proveedor.
- Devuelve códigos de error predecibles. Usa
400para entradas malformadas,401o403por verificación fallida, y5xxsolo cuando el problema sea tu sistema. Así es más fácil razonar sobre el comportamiento de reintento del proveedor. - Establece límites antes de parsear. Limita el tamaño de la petición, el content type y el número de headers desde el inicio. Esto evita que un endpoint de webhook se convierta en un agujero de ingesta genérico.
- Mantén el contrato acotado. Acepta solo los campos y tipos de eventos que soportes. Un parsing permisivo parece cómodo al principio, pero sale caro cuando cambias de proveedor API.
Checks de observabilidad
Las buenas operaciones de webhooks parecen aburridas. Los equipos pueden responder tres preguntas rápido: ¿Lo recibimos? ¿Lo verificamos? ¿El procesamiento downstream salió bien?
Sigue este estándar:
- Rastrea la recepción, verificación y procesamiento como resultados separados.
- Loguea request IDs, event IDs, el estado de la firma y el desfase de timestamp.
- Mide la demora de la cola, la latencia del handler y el volumen de reintentos.
- Mantén una ruta de reenvío segura para staging o flujos de redelivery.
- Alerta ante cambios de patrón, como un pico en fallos de firma o entregas duplicadas.
Capgo es un buen ejemplo del punto operativo más amplio. Incluye herramientas para la entrega de releases y observabilidad en su flujo de actualización, y partes de su ecosistema también tocan flujos relacionados con webhooks. La lección es práctica: los sistemas de entrega necesitan visibilidad desde la recepción hasta la finalización.
Si un equipo cubre las verificaciones anteriores, el receptor de webhooks suele estar en buen estado para producción. Si falta algún elemento, esa brecha tiende a aparecer durante un incidente, no durante la demo.
Preguntas frecuentes sobre webhooks
¿Qué estado code debo devolver?
Devuelve un 2xx cuando hayas aceptado el webhook. Si la validación falla, devuelve un error de cliente o de autenticación que coincida con el fallo, como 400 para entradas malformadas o 401 para datos de autenticación inválidos. Mantén esa lógica consistente para que los dashboards del proveedor sean más fáciles de interpretar.
¿Debo procesar el webhook de forma síncrona?
Normalmente, no. Valídalo, acúsalo de recibo y luego envía el trabajo real a una cola o a un worker en segundo plano. Esto mantiene la ruta de entrega rápida y reduce los reintentos duplicados causados por un procesamiento lento aguas abajo.
¿Cómo debo manejar los reintentos?
Asume que ocurrirán. Incorpora idempotencia en tu handler para que recibir el mismo evento de nuevo no duplique efectos secundarios. Los IDs de evento o los IDs de entrega del proveedor son los anclajes habituales para eso.
¿Qué pasa si los eventos llegan fuera de orden?
Diseña los handlers para que toleren el desorden cuando sea posible. Si el proceso de negocio requiere secuencia, persiste suficiente estado para detectar transiciones obsoletas en lugar de asumir que el orden de entrega refleja el orden de los eventos.
¿Cómo manejo los cambios de versión de los webhooks?
Versiona la lógica de tus handlers de forma deliberada. Mantén aislado el parsing específico del proveedor, evita dispersar suposiciones sobre el payload por tu codebase y añade pruebas con muestras reales capturadas antes de desplegar soporte para un nuevo formato.
Si tu equipo publica apps de Capacitor o Electron, Capgo vale la pena conocerlo por una razón relacionada. Ofrece a los equipos una forma controlada de entregar actualizaciones web firmadas, observar el comportamiento del rollout y recuperarse de incidentes sin esperar a la revisión de la app store, lo cual encaja con el mismo instinto de ingeniería detrás de un diseño sólido de webhooks: validar entradas, mantener las rutas de release observables y hacer la recuperación rápida.