Un chatbot de IA que puede responder preguntas es útil. Un chatbot que puede responder preguntas, crear tickets de soporte, buscar pedidos, actualizar registros CRM y activar workflows es transformador. El puente entre un chatbot independiente y uno integrado son los webhooks y las APIs.
Esta guía recorre los fundamentos de las integraciones basadas en webhooks, patrones comunes para conectar chatbots con sistemas externos y un tutorial completo paso a paso para crear un webhook que cree tickets de soporte cuando el chatbot no pueda resolver un problema.
Resumen rápido:
- Los webhooks son callbacks HTTP que envían eventos desde tu chatbot a sistemas externos en tiempo real. Protégelos con firmas HMAC y valida cada payload entrante.
- Patrones comunes de integración incluyen sincronización CRM (registrar conversaciones), creación de tickets (escalar problemas no resueltos), búsqueda de pedidos (traer datos en tiempo real) y actualizaciones de base de conocimiento (mantener contenido fresco).
- Crea integraciones con lógica de retry y exponential backoff desde el primer día. La entrega de webhooks falla más a menudo de lo que esperas, y los fallos silenciosos erosionan la confianza.
Nuestra metodología
Esta guía sintetiza detalles operativos de tres categorías de fuentes:
- Patrones de código de producción de repos open-source (por ejemplo, LangChain, LlamaIndex, documentación de pgvector y ejemplos de HuggingFace)
- Investigación académica publicada en arxiv y en actas de conferencias sobre recuperación y generación
- Debates de profesionales en r/MachineLearning, r/LocalLLaMA y r/LangChain donde ingenieros reportan restricciones reales de producción alrededor de webhooks e integraciones de chatbots
Evitamos afirmaciones puramente de marketing y priorizamos ejemplos que se despliegan en bases de código reales. Cuando citamos cifras de latencia o precisión, la metodología, dataset o condiciones de prueba se indican junto a ellas. Revisado por última vez: abril de 2026.
Fundamentos de webhooks
Un webhook es una solicitud HTTP POST enviada de un sistema a otro cuando ocurre un evento. A diferencia de polling (preguntar repetidamente "¿hay algo nuevo?"), los webhooks empujan eventos cuando suceden.
┌──────────────┐ Ocurre evento ┌──────────────────┐
│ Plataforma │ ──── HTTP POST ────▶ │ Tu endpoint │
│ chatbot │ │ webhook │
└──────────────┘ └────────┬─────────┘
│
Procesar evento
│
┌────────▼─────────┐
│ Sistema externo │
│ (CRM, helpdesk, │
│ sistema pedidos) │
└──────────────────┘
Patrón de solicitud y respuesta
La plataforma de chatbot envía una solicitud POST con un payload JSON que describe el evento. Tu endpoint lo procesa y devuelve un código de estado 2xx para confirmar recepción.
typescript
// Lo que la plataforma de chatbot envía a tu webhook
{
"event": "conversation.escalated",
"timestamp": "2026-03-30T14:22:00Z",
"data": {
"conversationId": "conv_abc123",
"customerId": "cust_456",
"customerEmail": "user@example.com",
"messages": [
{ "role": "user", "content": "My order hasn't arrived and it's been 2 weeks" },
{ "role": "assistant", "content": "I apologize for the delay. Let me look into this..." },
{ "role": "user", "content": "I want to speak to someone" }
],
"metadata": {
"agentConfidence": 0.32,
"escalationReason": "customer_requested",
"detectedIntent": "shipping_complaint"
}
}
}
Tu endpoint debe responder rápido (menos de 5 segundos para la mayoría de plataformas). Si el procesamiento tarda más, acepta el webhook de inmediato y procesa de forma asíncrona:
typescript
// Bien: confirmar de inmediato, procesar async
app.post("/webhooks/chatbot", async (req, res) => {
// Validar firma primero (ver siguiente sección)
if (!verifySignature(req)) {
return res.status(401).json({ error: "Invalid signature" });
}
// Confirmar recepción
res.status(200).json({ received: true });
// Procesar de forma asíncrona
processWebhookEvent(req.body).catch((err) => {
console.error("Webhook processing failed:", err);
});
});
Proteger webhooks con firmas HMAC
Cualquiera que descubra tu URL de webhook puede enviar eventos falsos. Las firmas HMAC lo evitan. La plataforma de chatbot firma cada payload con un secreto compartido, y tu endpoint verifica la firma antes de procesar.
typescript
import crypto from "crypto";
import type { Request } from "express";
const WEBHOOK_SECRET = process.env.WEBHOOK_SECRET!;
function verifySignature(req: Request): boolean {
const signature = req.headers["x-webhook-signature"] as string;
if (!signature) return false;
const expectedSignature = crypto
.createHmac("sha256", WEBHOOK_SECRET)
.update(JSON.stringify(req.body))
.digest("hex");
// Comparación timing-safe para prevenir ataques de timing
return crypto.timingSafeEqual(
Buffer.from(signature, "hex"),
Buffer.from(expectedSignature, "hex")
);
}
Usa siempre timingSafeEqual en lugar de === para comparar firmas. Una comparación normal de strings filtra información sobre cuántos caracteres coinciden, algo que atacantes pueden explotar.
Idempotencia
Las plataformas de webhooks reintentan ante fallos, lo que significa que tu endpoint puede recibir el mismo evento varias veces. Diseña tu handler para ser idempotente: procesar el mismo evento dos veces debería producir el mismo resultado que procesarlo una vez.
typescript
import { Redis } from "ioredis";
const redis = new Redis(process.env.REDIS_URL);
async function processWebhookEvent(event: WebhookEvent): Promise<void> {
const eventId = event.data.conversationId + ":" + event.timestamp;
// Revisar si ya procesamos este evento
const alreadyProcessed = await redis.get(`webhook:processed:${eventId}`);
if (alreadyProcessed) {
console.log(`Skipping duplicate event: ${eventId}`);
return;
}
// Procesar el evento
await handleEvent(event);
// Marcar como procesado con TTL de 24 h
await redis.set(`webhook:processed:${eventId}`, "1", "EX", 86400);
}
Patrones comunes de integración
1. Sincronización CRM
Registra cada conversación del chatbot en tu CRM para que ventas y soporte tengan contexto completo.
typescript
interface CRMContact {
email: string;
properties: Record<string, string>;
}
async function syncConversationToCRM(event: ConversationEndedEvent): Promise<void> {
const { customerEmail, messages, metadata } = event.data;
// Crear o actualizar el contacto en tu CRM
const contact = await crmClient.contacts.createOrUpdate({
email: customerEmail,
properties: {
last_chatbot_interaction: new Date().toISOString(),
chatbot_resolution_status: metadata.resolved ? "resolved" : "unresolved",
chatbot_detected_intent: metadata.detectedIntent,
},
});
// Registrar la conversación como actividad/nota
await crmClient.activities.create({
contactId: contact.id,
type: "chatbot_conversation",
body: summarizeConversation(messages),
metadata: {
conversationId: event.data.conversationId,
messageCount: messages.length,
duration: metadata.durationSeconds,
},
});
}
2. Creación de tickets
Cuando el chatbot no puede resolver un problema, crea automáticamente un ticket de soporte con contexto completo.
typescript
async function createSupportTicket(event: EscalationEvent): Promise<string> {
const { customerId, customerEmail, messages, metadata } = event.data;
const ticket = await helpdeskClient.tickets.create({
subject: `Chatbot escalation: ${metadata.detectedIntent}`,
requester: { email: customerEmail },
priority: determinePriority(metadata),
tags: ["chatbot-escalation", metadata.detectedIntent],
description: formatTicketDescription(messages, metadata),
customFields: {
chatbot_conversation_id: event.data.conversationId,
escalation_reason: metadata.escalationReason,
ai_confidence_score: metadata.agentConfidence,
},
});
return ticket.id;
}
function determinePriority(metadata: EscalationMetadata): "low" | "normal" | "high" | "urgent" {
if (metadata.escalationReason === "customer_frustrated") return "high";
if (metadata.agentConfidence < 0.2) return "high";
if (metadata.detectedIntent.includes("billing")) return "normal";
return "normal";
}
function formatTicketDescription(
messages: Message[],
metadata: EscalationMetadata
): string {
const transcript = messages
.map((m) => `**${m.role === "user" ? "Customer" : "AI"}:** ${m.content}`)
.join("\n\n");
return `## Chatbot Escalation
**Reason:** ${metadata.escalationReason}
**AI Confidence:** ${(metadata.agentConfidence * 100).toFixed(0)}%
**Detected Intent:** ${metadata.detectedIntent}
## Conversation Transcript
${transcript}`;
}
3. Búsqueda de pedidos
El chatbot llama a tu API para obtener datos de pedido en tiempo real durante la conversación.
typescript
// Esta es una llamada API saliente desde el chatbot, no un webhook
// Registrada como una "herramienta" que el chatbot puede invocar
async function lookupOrder(orderId: string, customerEmail: string): Promise<OrderInfo> {
const response = await fetch(`${ORDER_API_URL}/orders/${orderId}`, {
headers: {
Authorization: `Bearer ${ORDER_API_TOKEN}`,
"Content-Type": "application/json",
},
});
if (!response.ok) {
throw new Error(`Order lookup failed: ${response.status}`);
}
const order = await response.json();
// Verificar que el pedido pertenece a este cliente
if (order.customerEmail !== customerEmail) {
throw new Error("Order does not belong to this customer");
}
return {
orderId: order.id,
status: order.status,
trackingNumber: order.trackingNumber,
estimatedDelivery: order.estimatedDelivery,
items: order.items.map((i: OrderItem) => ({
name: i.productName,
quantity: i.quantity,
})),
};
}
4. Actualizaciones de base de conocimiento
Cuando cambia tu documentación, activa un webhook para reindexar el contenido actualizado en la base de conocimiento de tu chatbot.
typescript
// Webhook desde tu CMS cuando se publica o actualiza un artículo de ayuda
app.post("/webhooks/cms-update", async (req, res) => {
if (!verifySignature(req)) {
return res.status(401).json({ error: "Invalid signature" });
}
res.status(200).json({ received: true });
const { articleId, action, content, title, url } = req.body;
switch (action) {
case "published":
case "updated":
await chatbotKB.upsertDocument({
externalId: articleId,
title,
content,
sourceUrl: url,
lastUpdated: new Date().toISOString(),
});
break;
case "unpublished":
await chatbotKB.deleteDocument({ externalId: articleId });
break;
}
});
Tutorial paso a paso: webhook de ticket de escalado
Construyamos un endpoint webhook completo que crea un ticket de soporte en un sistema externo cuando el chatbot escala una conversación. Usaremos Express, TypeScript y una API genérica de helpdesk.
Configuración del proyecto
bash
mkdir chatbot-webhook && cd chatbot-webhook
npm init -y
npm install express dotenv
npm install -D typescript @types/express @types/node tsx
npx tsc --init
El servidor webhook completo
typescript
// src/server.ts
import express from "express";
import crypto from "crypto";
import dotenv from "dotenv";
dotenv.config();
const app = express();
app.use(express.json());
// --- Configuración ---
const PORT = process.env.PORT || 3000;
const WEBHOOK_SECRET = process.env.WEBHOOK_SECRET!;
const HELPDESK_API_URL = process.env.HELPDESK_API_URL!;
const HELPDESK_API_KEY = process.env.HELPDESK_API_KEY!;
// --- Tipos ---
interface WebhookPayload {
event: string;
timestamp: string;
data: {
conversationId: string;
customerId: string;
customerEmail: string;
customerName?: string;
messages: Array<{ role: "user" | "assistant"; content: string }>;
metadata: {
agentConfidence: number;
escalationReason: string;
detectedIntent: string;
sessionDurationSeconds: number;
};
};
}
interface TicketResponse {
id: string;
url: string;
status: string;
}
// --- Verificación de firma ---
function verifyWebhookSignature(
payload: string,
signature: string | undefined
): boolean {
if (!signature) return false;
const expected = crypto
.createHmac("sha256", WEBHOOK_SECRET)
.update(payload)
.digest("hex");
try {
return crypto.timingSafeEqual(
Buffer.from(signature, "hex"),
Buffer.from(expected, "hex")
);
} catch {
return false;
}
}
// --- Cliente API de helpdesk ---
async function createTicket(payload: WebhookPayload): Promise<TicketResponse> {
const { data } = payload;
const ticketBody = {
subject: `[Chatbot Escalation] ${data.metadata.detectedIntent}`,
requester: {
email: data.customerEmail,
name: data.customerName || "Customer",
},
priority: data.metadata.agentConfidence < 0.3 ? "high" : "normal",
tags: ["chatbot-escalation", data.metadata.detectedIntent],
description: buildTicketDescription(data),
custom_fields: {
conversation_id: data.conversationId,
escalation_reason: data.metadata.escalationReason,
ai_confidence: data.metadata.agentConfidence,
session_duration: data.metadata.sessionDurationSeconds,
},
};
const response = await fetch(`${HELPDESK_API_URL}/tickets`, {
method: "POST",
headers: {
Authorization: `Bearer ${HELPDESK_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify(ticketBody),
});
if (!response.ok) {
const errorBody = await response.text();
throw new Error(
`Helpdesk API error: ${response.status} - ${errorBody}`
);
}
return response.json() as Promise<TicketResponse>;
}
function buildTicketDescription(
data: WebhookPayload["data"]
): string {
const transcript = data.messages
.map((m) => {
const speaker = m.role === "user" ? "Customer" : "AI Agent";
return `**${speaker}:** ${m.content}`;
})
.join("\n\n");
return `## Escalation Details
| Field | Value |
|-------|-------|
| Conversation ID | ${data.conversationId} |
| Escalation Reason | ${data.metadata.escalationReason} |
| AI Confidence | ${(data.metadata.agentConfidence * 100).toFixed(0)}% |
| Detected Intent | ${data.metadata.detectedIntent} |
| Session Duration | ${data.metadata.sessionDurationSeconds}s |
## Conversation Transcript
${transcript}`;
}
// --- Endpoint webhook ---
app.post("/webhooks/escalation", (req, res) => {
// Paso 1: verificar firma
const rawBody = JSON.stringify(req.body);
const signature = req.headers["x-webhook-signature"] as string;
if (!verifyWebhookSignature(rawBody, signature)) {
console.warn("Invalid webhook signature received");
return res.status(401).json({ error: "Invalid signature" });
}
const payload = req.body as WebhookPayload;
// Paso 2: validar tipo de evento
if (payload.event !== "conversation.escalated") {
return res.status(200).json({ skipped: true, reason: "Unhandled event type" });
}
// Paso 3: confirmar de inmediato
res.status(200).json({ received: true, conversationId: payload.data.conversationId });
// Paso 4: procesar de forma asíncrona
createTicket(payload)
.then((ticket) => {
console.log(
`Ticket created: ${ticket.id} for conversation ${payload.data.conversationId}`
);
})
.catch((err) => {
console.error(
`Failed to create ticket for ${payload.data.conversationId}:`,
err
);
// En producción: enviar a una dead-letter queue para retry
});
});
// Health check
app.get("/health", (_, res) => {
res.status(200).json({ status: "ok" });
});
app.listen(PORT, () => {
console.log(`Webhook server listening on port ${PORT}`);
});
Variables de entorno
bash
# .env
PORT=3000
WEBHOOK_SECRET=your-shared-secret-from-chatbot-platform
HELPDESK_API_URL=https://api.yourhelpdesk.com/v2
HELPDESK_API_KEY=your-helpdesk-api-key
Ejecutarlo
bash
npx tsx src/server.ts
Patrones de autenticación
Distintos sistemas externos requieren distintos mecanismos de autenticación. Estos son los tres patrones más comunes:
API Keys
El enfoque más simple. Incluye una key estática en el header de la solicitud.
typescript
const response = await fetch(apiUrl, {
headers: {
"X-API-Key": process.env.EXTERNAL_API_KEY!,
"Content-Type": "application/json",
},
body: JSON.stringify(payload),
});
Pros: simple, sin lógica de refresh de token.
Contras: rotar la key requiere redeploy. Sin scoping ni expiración.
OAuth 2.0 Client Credentials
Para comunicación machine-to-machine donde tu servidor se autentica como sí mismo (no en nombre de un usuario).
typescript
class OAuth2Client {
private accessToken: string | null = null;
private tokenExpiry: number = 0;
constructor(
private clientId: string,
private clientSecret: string,
private tokenUrl: string
) {}
async getAccessToken(): Promise<string> {
// Devolver token cacheado si sigue siendo válido
if (this.accessToken && Date.now() < this.tokenExpiry - 60_000) {
return this.accessToken;
}
const response = await fetch(this.tokenUrl, {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: new URLSearchParams({
grant_type: "client_credentials",
client_id: this.clientId,
client_secret: this.clientSecret,
scope: "tickets:write contacts:read",
}),
});
if (!response.ok) {
throw new Error(`Token request failed: ${response.status}`);
}
const data = await response.json();
this.accessToken = data.access_token;
this.tokenExpiry = Date.now() + data.expires_in * 1000;
return this.accessToken;
}
async authenticatedFetch(url: string, options: RequestInit = {}): Promise<Response> {
const token = await this.getAccessToken();
return fetch(url, {
...options,
headers: {
...options.headers,
Authorization: `Bearer ${token}`,
},
});
}
}
JWT (JSON Web Tokens)
Genera tokens de vida corta firmados con tu clave privada. El sistema externo verifica la firma con tu clave pública.
typescript
import jwt from "jsonwebtoken";
function generateServiceToken(): string {
return jwt.sign(
{
iss: "chatbot-webhook-service",
sub: "webhook-integration",
aud: "helpdesk-api",
iat: Math.floor(Date.now() / 1000),
exp: Math.floor(Date.now() / 1000) + 300, // Expira en 5 minutos
},
process.env.JWT_PRIVATE_KEY!,
{ algorithm: "RS256" }
);
}
Manejo de errores y lógica de retry
La entrega de webhooks a APIs externas falla regularmente: timeouts de red, rate limits, errores transitorios de servidor. Construye lógica de retry desde el inicio.
Exponential backoff con jitter
typescript
interface RetryConfig {
maxRetries: number;
baseDelayMs: number;
maxDelayMs: number;
}
async function withRetry<T>(
fn: () => Promise<T>,
config: RetryConfig = { maxRetries: 3, baseDelayMs: 1000, maxDelayMs: 30000 }
): Promise<T> {
let lastError: Error | null = null;
for (let attempt = 0; attempt <= config.maxRetries; attempt++) {
try {
return await fn();
} catch (err) {
lastError = err as Error;
if (attempt === config.maxRetries) break;
// No reintentar errores de cliente (4xx excepto 429)
if (err instanceof ApiError && err.status >= 400 && err.status < 500 && err.status !== 429) {
throw err;
}
// Exponential backoff con jitter
const exponentialDelay = config.baseDelayMs * Math.pow(2, attempt);
const jitter = Math.random() * config.baseDelayMs;
const delay = Math.min(exponentialDelay + jitter, config.maxDelayMs);
console.warn(
`Attempt ${attempt + 1} failed, retrying in ${Math.round(delay)}ms:`,
(err as Error).message
);
await sleep(delay);
}
}
throw lastError;
}
function sleep(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms));
}
// Uso
const ticket = await withRetry(() => createTicket(payload), {
maxRetries: 3,
baseDelayMs: 1000,
maxDelayMs: 30000,
});
Dead-letter queue
Para eventos que fallan después de todos los retries, envíalos a una dead-letter queue para revisión manual o procesamiento posterior:
typescript
async function processWebhookWithDLQ(payload: WebhookPayload): Promise<void> {
try {
await withRetry(() => createTicket(payload));
} catch (err) {
console.error("All retries exhausted. Pushing to DLQ:", err);
await deadLetterQueue.push({
payload,
error: (err as Error).message,
failedAt: new Date().toISOString(),
retryCount: 3,
});
// Alertar al equipo
await alerting.notify({
channel: "webhook-failures",
message: `Webhook processing failed for conversation ${payload.data.conversationId}`,
severity: "warning",
});
}
}
Probar webhooks localmente
ngrok
Expón tu servidor local a internet para que la plataforma de chatbot pueda alcanzarlo:
bash
# Terminal 1: ejecutar tu servidor
npx tsx src/server.ts
# Terminal 2: exponer puerto 3000
ngrok http 3000
ngrok te da una URL pública como https://a1b2c3d4.ngrok.io. Regístrala como tu URL de webhook en la configuración de la plataforma de chatbot, añadiendo tu path: https://a1b2c3d4.ngrok.io/webhooks/escalation.
Prueba manual con curl
Envía un evento de prueba a tu endpoint local:
bash
# Generar una firma válida
SECRET="your-shared-secret"
PAYLOAD='{"event":"conversation.escalated","timestamp":"2026-03-30T14:22:00Z","data":{"conversationId":"conv_test123","customerId":"cust_456","customerEmail":"test@example.com","messages":[{"role":"user","content":"I need help"},{"role":"assistant","content":"Let me connect you with a human agent."}],"metadata":{"agentConfidence":0.25,"escalationReason":"low_confidence","detectedIntent":"general_inquiry","sessionDurationSeconds":45}}}'
SIGNATURE=$(echo -n "$PAYLOAD" | openssl dgst -sha256 -hmac "$SECRET" | awk '{print $2}')
curl -X POST http://localhost:3000/webhooks/escalation \
-H "Content-Type: application/json" \
-H "x-webhook-signature: $SIGNATURE" \
-d "$PAYLOAD"
webhook.site
Para inspeccionar qué está enviando realmente tu plataforma de chatbot, usa webhook.site. Te da una URL temporal que registra todas las solicitudes entrantes: headers, body, timing. Útil para depurar problemas de formato de payload antes de escribir código.
Monitoring y debugging en producción
Logging estructurado
Registra cada evento webhook con contexto suficiente para depurar fallos:
typescript
import pino from "pino";
const logger = pino({ level: "info" });
app.post("/webhooks/escalation", (req, res) => {
const conversationId = req.body?.data?.conversationId || "unknown";
const childLogger = logger.child({ conversationId, event: req.body?.event });
if (!verifyWebhookSignature(JSON.stringify(req.body), req.headers["x-webhook-signature"] as string)) {
childLogger.warn("Invalid webhook signature");
return res.status(401).json({ error: "Invalid signature" });
}
childLogger.info("Webhook received");
res.status(200).json({ received: true });
createTicket(req.body as WebhookPayload)
.then((ticket) => {
childLogger.info({ ticketId: ticket.id }, "Ticket created successfully");
})
.catch((err) => {
childLogger.error({ err }, "Ticket creation failed");
});
});
Métricas clave a rastrear
| Métrica | Qué te dice | Umbral de alerta |
|---|
| Tasa de recepción de webhooks | Volumen de eventos desde la plataforma de chatbot | Caída repentina (problema de plataforma) o pico (abuso) |
| Tasa de fallo de validación de firma | Posibles problemas de seguridad o mala configuración | >1% de solicitudes |
| Tasa de éxito de procesamiento | Qué tan fiable manejas eventos | <95% |
| Latencia de API externa | Qué tan rápido responde helpdesk/CRM | P95 > 5 s |
| Tasa de retry | Qué tan a menudo falla el primer intento | >10% indica problemas upstream |
| Profundidad de DLQ | Eventos que fallaron todos los retries | >0 requiere investigación |
Endpoint de health check
Expón un health check que verifique conectividad con dependencias externas:
typescript
app.get("/health", async (_, res) => {
const checks: Record<string, "ok" | "error"> = {};
try {
await fetch(`${HELPDESK_API_URL}/health`, {
headers: { Authorization: `Bearer ${HELPDESK_API_KEY}` },
signal: AbortSignal.timeout(3000),
});
checks.helpdesk = "ok";
} catch {
checks.helpdesk = "error";
}
try {
await redis.ping();
checks.redis = "ok";
} catch {
checks.redis = "error";
}
const allHealthy = Object.values(checks).every((v) => v === "ok");
res.status(allHealthy ? 200 : 503).json({ status: allHealthy ? "ok" : "degraded", checks });
});
Ideas clave
- Los webhooks empujan eventos en tiempo real desde tu chatbot a sistemas externos. Protégelos siempre con firmas HMAC y valida antes de procesar.
- Confirma rápido, procesa async. Devuelve 200 de inmediato y maneja el trabajo en segundo plano para evitar timeouts.
- Construye lógica de retry desde el primer día. Exponential backoff con jitter y una dead-letter queue para retries agotados te salva de pérdida silenciosa de datos.
- La idempotencia no es opcional. Las plataformas de webhooks reintentan, y tu handler debe producir el mismo resultado al procesar el mismo evento dos veces.
- Monitorea todo. Rastrea tasa de recepción, éxito de procesamiento, latencia de API externa y profundidad de DLQ. Los fallos silenciosos se acumulan rápido.
Cuándo los webhooks son la elección de integración equivocada
- Flujos síncronos request-response donde el llamador necesita la respuesta en el mismo turno HTTP, no un callback
- Streams de eventos de alta frecuencia donde el fan-out de webhooks sobrecarga servicios downstream y una queue encaja mejor
- Redes no confiables donde los receptores no pueden validar firmas y la protección contra replay se vuelve frágil
- Workflows que necesitan procesamiento ordenado y exactly-once, ya que la mayoría de entregas webhook son at-least-once y fuera de orden
- Integraciones hacia sistemas detrás de firewalls estrictos donde exponer un endpoint entrante no es viable
- Equipos sin observabilidad para retries, dead-letter queues y fallos de firma, donde los webhooks pierden eventos en silencio
Preguntas frecuentes
¿Cuál es la diferencia entre un webhook y una API?
Una API es un conjunto de endpoints que llamas cuando necesitas datos (modelo pull). Un webhook es un callback HTTP que un sistema remoto te envía cuando ocurre un evento (modelo push). En integraciones de chatbots, usas APIs para obtener datos (por ejemplo, buscar un pedido) y webhooks para reaccionar a eventos (por ejemplo, crear un ticket cuando una conversación se escala). La mayoría de integraciones usan ambos: los webhooks disparan el workflow y las llamadas API dentro de ese workflow interactúan con sistemas externos.
¿Cómo protejo mi endpoint webhook?
Usa firmas HMAC. La plataforma de chatbot firma cada payload con un secreto compartido usando HMAC-SHA256. Tu endpoint recalcula la firma desde el cuerpo crudo de la solicitud y el mismo secreto, luego las compara con una función timing-safe. Rechaza cualquier solicitud donde la firma no coincida. Además, usa HTTPS para cifrado de transporte, restringe tu endpoint a rangos IP esperados si tu plataforma los publica y valida el esquema del payload antes de procesar.
¿Qué pasa si mi endpoint webhook está caído?
La mayoría de plataformas de chatbots implementan retries automáticos con exponential backoff. Si tu endpoint devuelve un código no 2xx o hace timeout, la plataforma reintenta, normalmente 3-5 veces durante varias horas. Los eventos que agotan todos los retries suelen registrarse en el dashboard de entregas webhook de la plataforma. De tu lado, diseña para idempotencia para que cuando el endpoint se recupere y lleguen retries, el procesamiento duplicado no cree tickets o registros duplicados.
¿Cómo pruebo webhooks durante desarrollo local?
Usa ngrok para exponer tu servidor local a internet. Ejecuta tu servidor en localhost, luego ejecuta ngrok http 3000 para obtener una URL pública. Registra esa URL como tu endpoint webhook en la plataforma de chatbot. Para pruebas manuales, usa curl con una firma HMAC precomputada para enviar payloads de prueba directamente. Para inspeccionar los payloads crudos que envía tu plataforma, usa webhook.site para capturar y mostrar solicitudes entrantes.
¿Cómo manejo eventos webhook que requieren procesamiento lento?
Acepta el webhook de inmediato con una respuesta 200 y luego procesa el evento de forma asíncrona. Para casos simples, usa una llamada async después de responder. Para sistemas de producción, envía el evento a una queue de mensajes (como SQS, Redis Streams o BullMQ) y procésalo con un worker separado. Esto desacopla recepción de procesamiento, evita timeouts y te permite escalar workers de forma independiente según profundidad de queue.
Artículos relacionados