# El webhook de sincronización > Un endpoint firmado por base de conocimiento. Qué es, cuándo usarlo, y todos sus campos, cabeceras y errores. Source: https://welcomeai.dev/es/docs/sync-webhook Category: Integraciones Updated: 2026-09-06 --- ## 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 | Quieres | Usa | |---|---| | Sincronizar un sitio WordPress o Webflow | Su conector | | Rastrear una web pública | El rastreador | | Empujar desde tu backend, con tu código | La API REST con una clave | | Que Zapier, Make o n8n empujen, sin código | **Este webhook** | 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 inválida 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="> ``` 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: ```json { "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" } } ``` ```json { "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 | Campo | Obligatorio | Qué es | |---|---|---| | `external_id` | **sí** | Tu identificador para este documento, hasta 500 caracteres. Volver a enviar el mismo lo actualiza en vez de crear una copia. Es el campo más importante de acertar. | | `content_md` o `content_html` | sí, salvo al borrar | El contenido. Markdown preferido; el HTML se convierte. Hasta 2 MB. | | `title` | no | Lo que muestra una cita. Vale la pena ponerlo: un "Sin título" en una cita hace que una respuesta correcta parezca equivocada. | | `url` | no | Dónde puede verse el original. Se convierte en el enlace de la cita. | | `excerpt` | no | Un resumen corto, guardado junto al documento. | | `metadata` | no | Cualquier objeto JSON tuyo. Se conserva con el documento. | | `content_hash` | no | Si lo envías, un hash sin cambios se salta la reindexación entera. Barato y recomendable en una sincronización grande. | | `action` | no | `upsert` (por defecto) o `delete`. | 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. ```json { "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 | Estado | Código | Qué pasó | |---|---|---| | 400 | `invalid_json` | El cuerpo no es JSON | | 400 | `invalid_input` | Falta un campo o tiene la forma equivocada. El mensaje lo nombra | | 401 | `invalid_signature` | La firma, el secreto o el timestamp están mal | | 404 | `not_found` | No hay webhook activo para esa base, o el token no coincide | | 409 | `source_paused` | La fuente está pausada. Reanúdala en Fuentes conectadas | | 413 | `payload_too_large` | El cuerpo pasa de 4 MB | 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_id` distintos, 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.