El webhook de sincronización
Un endpoint firmado por base de conocimiento. Qué es, cuándo usarlo, y todos sus campos, cabeceras y errores.
Qué es un webhook
Un webhook es una URL que le das a otro sistema para que te llame a ti cuando pasa algo. La alternativa es preguntar cada pocos minutos si hay novedades, que es más lento y casi siempre inútil. Un webhook es una petición, en el momento en que la cosa cambió de verdad.
Qué hace este
Cada base de conocimiento puede tener un endpoint firmado. Envías un documento y el documento se añade, se actualiza o se elimina. Ese es todo el contrato.
Existe para contenido que vive en algún sitio para el que no tenemos conector: un CMS interno, una base de Notion pasando por Zapier, una hoja de cálculo en Make, un script que se ejecuta después de tu build. Si ya usas WordPress o Webflow, usa esos conectores: saben qué cambió y gestionan los borrados por ti.
Cuándo usarlo y cuándo no
La diferencia con la API REST es solo cómo demuestras quién eres. La API REST usa una clave en una cabecera, que es lo correcto cuando controlas el código. Este usa una firma, que es lo correcto cuando quien llama es una herramienta sin código: la URL lleva un token público, así que la herramienta no necesita guardar credenciales, y la firma demuestra que el cuerpo no se manipuló.
Activarlo
Integraciones, luego Webhook de sincronización. Elige una base de conocimiento y pulsa Activar. Obtienes dos cosas:
- El endpoint,
https://app.welcomeai.dev/api/v1/kb/{kbId}/sync?t=whsec_…. El token de la URL identifica qué fuente se alimenta. Por sí solo no es un secreto: nombra, no autoriza. - El secreto de firma, que se muestra una sola vez. Guárdalo donde tu automatización guarde credenciales.
Rotar el secreto invalida el anterior al instante. No hay ventana de solape, así que cámbialo antes en tu automatización o asume unas peticiones fallidas.
Autenticación
Firma el cuerpo en crudo con HMAC-SHA256 y envía el resumen en hexadecimal.
X-WelcomeAI-Timestamp: 1757155200
X-WelcomeAI-Signature: sha256=<hex de "1757155200.<cuerpo en crudo>">Lo que se firma es el timestamp, un punto literal y el cuerpo exactamente como viaja. Firma los bytes que envías: volver a serializar el JSON cambia los espacios y la firma dejará de coincidir.
Un timestamp de más de cinco minutos de antigüedad se rechaza, para que una petición capturada no pueda reproducirse después. Si tu herramienta no puede añadir cabeceras, omite el timestamp y firma solo el cuerpo; funciona igual y es lo que hace la mayoría de configuraciones de Zapier, pero renuncias a la protección contra reenvíos.
La petición
POST, Content-Type: application/json. Un documento, o un lote:
{
"external_id": "faq-reembolsos",
"title": "Reembolsos",
"url": "https://acme.com/ayuda/reembolsos",
"content_md": "# Reembolsos\n\nDentro de 14 días, escribe a billing@acme.com.",
"metadata": { "categoria": "facturacion" }
}{
"items": [
{ "external_id": "faq-reembolsos", "title": "Reembolsos", "content_md": "…" },
{ "external_id": "faq-envios", "title": "Envíos", "content_md": "…" },
{ "external_id": "faq-viejo", "action": "delete" }
]
}Campos
Un lote son como mucho 25 elementos y el cuerpo entero como mucho 4 MB.
Borrar
Envía {"external_id": "…", "action": "delete"}. El documento, sus fragmentos y sus embeddings desaparecen. Borrar algo que nunca existió no es un error: devuelve missing y sigue, así que un borrado repetido es seguro.
La respuesta
200, siempre con el detalle por elemento. Un fallo parcial es un 200 con ok: false, no un 500: un elemento malo en un lote de veinticinco no debe obligarte a reenviar los otros veinticuatro.
{
"ok": true,
"accepted": 2,
"failed": 0,
"results": [
{ "external_id": "faq-reembolsos", "status": "created" },
{ "external_id": "faq-envios", "status": "unchanged" }
]
}status es created, updated, unchanged, deleted, missing o failed. Un elemento failed lleva un error que dice por qué.
`unchanged` es el que conviene mirar. Significa que el hash coincidía con lo que ya teníamos y no se reindexó nada. En una sincronización nocturna completa casi todo debería decir unchanged; si todo dice updated, algo en tu proceso está reescribiendo el contenido en cada pasada -- normalmente una marca de tiempo regenerada dentro del cuerpo -- y estás pagando por volver a incrustar el mismo texto.
Errores
El 401 es el que sale de verdad. Por orden de probabilidad: se volvió a serializar el cuerpo antes de firmar; el timestamp se puso en la cabecera pero no en lo firmado, o al revés; se rotó el secreto; el reloj va desviado más de cinco minutos.
Reintentos
Nosotros no reintentamos: el que llama eres tú. Reintenta un 5xx o un fallo de red con espera creciente, y no reintentes un 4xx: nada de esa petición va a funcionar a la segunda.
Reintentar es seguro. external_id hace que cada escritura sea idempotente, así que enviar dos veces el mismo documento produce un documento, y la segunda llamada devuelve unchanged.
Límites
Hoy no hay límite de peticiones por endpoint. La ingesta la acotan los límites de documentos y almacenamiento de tu plan, y un lote está limitado a 25 elementos y 4 MB. Sé razonable en una primera importación: unas pocas peticiones por segundo, no unos cientos.
Mantenerlo seguro
- Trata el secreto como una contraseña. Es lo único que separa tu base de conocimiento de cualquiera que tenga la URL, y la URL viaja por la herramienta donde la pegaste.
- Rótalo cuando alguien se va, o cuando se retira una automatización.
- Una base, un webhook. Si dos sistemas alimentan la misma base, dales bases separadas o prefijos de
external_iddistintos, para poder saber después cuál envió qué. - No envíes nada que un visitante no deba leer. Todo lo que publiques es contenido que el asistente puede citarle a quien pregunte.
- Cada petición queda en el Registro de sincronización con qué encontró y qué cambió, que es donde mirar cuando el contenido no es el que esperabas.