# Enviar leads y traspasos a tus herramientas con webhooks

> Envía leads nuevos, traspasos y peticiones de chat en vivo, formularios enviados, citas reservadas y peticiones de devolución de intoCHAT a tu servidor, Zapier, Make o n8n como JSON firmado: eventos, contenido, firmas y reintentos.

Un webhook envía un evento de tu agente a la dirección que elijas, como una petición JSON firmada por `POST`, en cuestión de segundos. Úsalo para llevar los leads nuevos a tu CRM o para iniciar una automatización. Funciona con tu propio servidor y con herramientas de automatización que aceptan webhooks entrantes, como Zapier, Make o n8n: usas el disparador de la herramienta para webhooks entrantes. Las apps propias de intoCHAT para Zapier y Make están construidas y estarán disponibles en cuanto se publiquen en los directorios de esas herramientas; consulta [Zapier y Make](/es/docs/zapier-and-make). Para recibir estos eventos como mensajes en un canal de Slack no necesitas un webhook: usa los [avisos en Slack](/es/docs/lead-alerts-and-export).

## Eventos

| Evento | Nombre en el panel | Se envía cuando |
| --- | --- | --- |
| `lead.created` | **New lead** | se guarda un contacto nuevo, desde el [formulario de leads](/es/docs/lead-collection) o a partir de datos que el visitante escribió en el chat. |
| `handoff.requested` | **Hand-off requested** | el agente envió una conversación a tu equipo por correo. Consulta [Traspaso por correo](/es/docs/handoff). |
| `form.submitted` | **Form submitted** | un visitante envió uno de tus [formularios de chat](/es/docs/chat-forms). |
| `booking.created` | **Meeting booked** | un visitante reservó una cita desde el chat. Consulta [Reservas con Cal.com o Calendly](/es/docs/booking). |
| `live_chat.requested` | **Live chat requested** | el agente pidió a tu equipo que se uniera al chat, porque un visitante pidió hablar con una persona mientras alguien de tu equipo estaba en línea. Consulta [Chat en vivo](/es/docs/live-chat). |
| `return.requested` | **Return requested** | un visitante pidió devolver o cambiar artículos de un pedido que el agente verificó. Consulta [Estado de pedidos y devoluciones](/es/docs/orders). |

`lead.created` se envía una vez por contacto. Un visitante que vuelve y envía los mismos datos no genera un evento nuevo, como tampoco un chat que añade un teléfono a un contacto ya guardado en ese chat.

`handoff.requested` solo se envía si el correo a tu equipo salió de verdad. Los traspasos desde el **Playground** también lo envían, con `"test": true`. Si conectaste los [tickets de helpdesk](/es/docs/helpdesk-tickets), se envía cuando el ticket se ha creado o ha fallado, normalmente unos segundos después, y lleva el ticket cuando lo hay.

`form.submitted` se envía una vez por cada envío. Un formulario solo se puede enviar una vez por conversación, y un envío nunca genera `lead.created`, aunque el formulario pida una dirección de correo.

`booking.created` se envía una vez por reserva, después de que Cal.com o Calendly la haya aceptado y de que intoCHAT la haya guardado. Una reserva nunca genera `lead.created`. Las reservas desde el **Playground** son reservas reales y también lo envían, sin marca de prueba. Los cambios que se hagan después en Cal.com o Calendly, como una cancelación, no envían ningún evento.

`live_chat.requested` se envía una vez por petición, cuando el agente pide a tu equipo que se una, tanto si alguien se une como si no. Un visitante que vuelve a pedirlo después de que la petición caducara o de que terminara el chat en vivo genera una nueva. El **Playground** nunca pide el chat en vivo, así que nunca envía este evento.

`return.requested` se envía una vez por petición de devolución, cuando intoCHAT la ha guardado. Cada conversación puede enviar una petición por pedido, así que volver a pedirlo para el mismo pedido no envía nada. Las peticiones desde el **Playground** también lo envían, sin marca de prueba.

## Añadir un webhook

1. Abre tu agente y ve a la pestaña **Leads**. **Webhooks** está hacia el final de la pestaña, encima de **Slack alerts**.
2. Haz clic en **Add webhook**.
3. En **Endpoint URL**, pega la dirección que te da tu servidor o tu herramienta para webhooks entrantes.
4. En **Events to send**, deja marcados los eventos que quieras: **New lead**, **Hand-off requested**, **Form submitted**, **Meeting booked**, **Live chat requested** y **Return requested**. En un webhook nuevo están marcados los seis. Un webhook que añadiste antes conserva sus eventos: haz clic en **Edit** y marca **Form submitted**, **Meeting booked**, **Live chat requested** o **Return requested** si debe recibirlos.
5. Deja **Active** activado y haz clic en **Add webhook**.
6. Copia el secreto de firma que aparece, guárdalo donde tu receptor pueda leerlo y haz clic en **I've saved it**.
7. Haz clic en **Send test event** para comprobar la conexión. El resultado aparece debajo de los botones.

La dirección tiene que empezar por `https://` y apuntar a un servidor público. Las direcciones de redes privadas o internas, como `localhost` o `192.168.1.10`, se rechazan al guardar y se comprueban de nuevo antes de cada envío. Cada agente puede tener hasta 5 webhooks.

Los webhooks que Zapier, Make o tu propio código suscribieron a través de la API REST no cuentan para esos 5; consulta [Webhooks creados por integraciones](#webhooks-creados-por-integraciones). Tampoco cuentan los webhooks de formularios concretos; consulta [Un webhook para un formulario](#un-webhook-para-un-formulario).

Para pausar un webhook sin perder su configuración, desactívalo: mostrará **Paused**. Los eventos que ocurren durante la pausa no se envían, ni siquiera después. Un envío que espera un reintento falla en su siguiente intento; cuando el webhook vuelva a estar activo, puedes reenviarlo. **Edit** cambia la dirección y los eventos. **Remove** elimina el webhook y su historial de envíos.

## Webhooks creados por integraciones

Una integración puede suscribir un webhook a tu agente a través de la [API REST](/es/docs/rest-api#suscribir-un-webhook), con una de tus claves de API. Las [apps de Zapier y Make](/es/docs/zapier-and-make) lo hacen cuando activas un Zap o añades un disparador instantáneo a un escenario.

Estos webhooks aparecen en la tarjeta **Webhooks** junto a los demás, con una etiqueta que indica de dónde vienen: **via Zapier**, **via Make** o **via API**. Se envían, se firman, se reintentan y se registran como los webhooks que añades tú, y puedes usar en ellos **Send test event**, **Recent deliveries**, el interruptor **Active** y **New secret**.

- No tienen botón **Edit**: el Zap, el escenario o el código que los creó es el dueño de la dirección y de los eventos, y cambiarlos aquí lo rompería.
- **Remove** elimina uno. El Zap o el escenario sigue activo, pero no recibe más eventos; desactívalo también allí, o desactívalo y vuelve a activarlo para suscribir un webhook nuevo.
- La integración elimina su webhook por sí misma cuando desactivas el Zap o eliminas el disparador.
- Revocar la clave de API que los creó los elimina. Consulta [Revocar una clave](/es/docs/rest-api#revocar-una-clave).
- Un agente puede tener hasta 25, aparte de los 5 que añades tú.

## Un webhook para un formulario

Un [formulario de chat](/es/docs/chat-forms#un-webhook-para-un-formulario) puede tener un webhook propio, que solo recibe los eventos `form.submitted` de ese formulario. Lo añades y lo gestionas en el editor del formulario, en la pestaña **Actions**, en **Webhook for this form**, no en la tarjeta **Webhooks**.

- Tiene su propio secreto de firma, que se muestra una sola vez, y se envía, se firma, se reintenta y se registra exactamente igual que los webhooks de esta página, con el mismo envoltorio y los mismos datos de `form.submitted`.
- Solo envía `form.submitted`, y solo para su formulario. Sus eventos no se pueden cambiar, y un evento de prueba funciona como se explica más abajo.
- No cuenta para los 5 webhooks del agente y no aparece en la tarjeta **Webhooks**.
- Los webhooks del agente suscritos a **Form submitted** siguen recibiendo los envíos de todos los formularios, también los de un formulario con webhook propio. Cada webhook recibe su propio envío, con su propio ID.
- Al borrar el formulario se borran su webhook y su historial de envíos.

## El secreto de firma

Cada webhook tiene su propio secreto de firma, que empieza por `whsec_`. Solo se muestra completo una vez, justo después de añadir el webhook o de crear un secreto nuevo. Después, la tarjeta solo muestra sus cuatro últimos caracteres.

Si pierdes el secreto o crees que alguien más lo conoce, haz clic en **New secret**. El secreto anterior deja de funcionar al momento, así que pasa el nuevo a tu receptor enseguida.

## Qué se envía

Cada envío es una petición `POST` con un cuerpo JSON y estas cabeceras:

| Cabecera | Valor |
| --- | --- |
| `Content-Type` | `application/json` |
| `User-Agent` | `intoCHAT-Webhooks/1.0` |
| `X-IntoChat-Event` | El nombre del evento, por ejemplo `lead.created` |
| `X-IntoChat-Delivery` | El ID del envío. Es el mismo en cada reintento. |
| `X-IntoChat-Timestamp` | Cuándo se envió este intento, en segundos Unix |
| `X-IntoChat-Signature` | `sha256=` seguido de la firma; ver más abajo |

El cuerpo siempre tiene el mismo envoltorio. `id` es el ID del envío, el mismo valor que `X-IntoChat-Delivery`:

```json
{
  "id": "DELIVERY_ID",
  "event": "lead.created",
  "created_at": "2026-10-07T09:30:00.000Z",
  "agent": { "id": "AGENT_ID", "name": "Acme Assistant" },
  "data": { }
}
```

### Datos de `lead.created`

| Campo | Contenido |
| --- | --- |
| `leadId` | El ID del lead |
| `agentId`, `agentName` | El agente que captó el lead |
| `email` | El correo electrónico, o `null` |
| `phone` | El teléfono, o `null` |
| `name` | El nombre del formulario, o `null`. Nunca se toma un nombre de un mensaje del chat. |
| `customFields` | Las respuestas a tus campos propios, como lista de `{ "id", "label", "value" }`. Vacía si no hay ninguna. |
| `source` | `form` o `chat` |
| `conversationId` | La conversación de la que viene el lead, o `null` |
| `collectedAt` | Cuándo se guardó el lead, como hora ISO 8601 en UTC |
| `identity` | Solo para un visitante verificado con la [verificación de identidad](/es/docs/identity-verification): `{ "userId", "name", "email" }`. Si no, no aparece. |

### Datos de `handoff.requested`

| Campo | Contenido |
| --- | --- |
| `conversationId` | La conversación traspasada |
| `visitorEmail` | La dirección que dio el visitante para la respuesta |
| `summary` | Lo que necesita el visitante, con las palabras del agente |
| `page` | La página donde empezó el chat, o `null` |
| `country` | El código de país del visitante, de dos letras, o `null` |
| `requestedAt` | Cuándo se envió el traspaso, como hora ISO 8601 en UTC |
| `test` | `true` en un traspaso desde el Playground; si no, `false` |
| `ticket` | Solo cuando los [tickets de helpdesk](/es/docs/helpdesk-tickets) abrieron un ticket para este traspaso: `{ "provider", "id", "url" }`, donde `provider` es `zendesk`, `freshdesk` o `hubspot`, `id` es el número del ticket como cadena de texto y `url` lo abre en tu helpdesk. Si no, no aparece. |

### Datos de `form.submitted`

| Campo | Contenido |
| --- | --- |
| `submissionId` | El ID del envío |
| `formId`, `formName` | El formulario enviado |
| `agentId`, `agentName` | El agente que mostró el formulario |
| `answers` | Las respuestas del visitante, como lista de `{ "id", "label", "value" }`, en el orden del formulario. Los campos opcionales que quedaron vacíos no aparecen. |
| `conversationId` | La conversación en la que se envió el formulario |
| `submittedAt` | Cuándo se guardó el envío, como hora ISO 8601 en UTC |
| `identity` | Solo para un visitante verificado con la [verificación de identidad](/es/docs/identity-verification): `{ "userId", "name", "email" }`. Si no, no aparece. |

### Datos de `booking.created`

| Campo | Contenido |
| --- | --- |
| `bookingId` | El ID de la reserva en intoCHAT |
| `provider` | El calendario: `calcom` o `calendly` |
| `providerBookingId` | El ID de la reserva en Cal.com, o la URI del invitado en Calendly, por ejemplo `https://api.calendly.com/scheduled_events/…/invitees/…` |
| `agentId`, `agentName` | El agente con el que se reservó la cita |
| `eventTypeId`, `eventTypeUri`, `eventTitle` | El tipo de evento reservado: `eventTypeId` es el número de Cal.com y `eventTypeUri` es la URI de Calendly. El otro es `null`. |
| `startAt`, `endAt` | Cuándo empieza y termina la cita, como horas ISO 8601 en UTC |
| `timeZone` | La zona horaria del visitante, por ejemplo `Europe/Madrid`, o `UTC` si su navegador no indicó ninguna |
| `attendeeName`, `attendeeEmail` | El nombre y el correo que introdujo el visitante |
| `providerStatus` | `accepted`, o `pending` si primero tienes que confirmar la reserva en Cal.com. Las reservas de Calendly siempre son `accepted`. |
| `conversationId` | La conversación en la que se reservó la cita |
| `createdAt` | Cuándo se guardó la reserva, como hora ISO 8601 en UTC |
| `identity` | Solo para un visitante verificado con la [verificación de identidad](/es/docs/identity-verification): `{ "userId", "name", "email" }`. Si no, no aparece. |

### Datos de `live_chat.requested`

| Campo | Contenido |
| --- | --- |
| `conversationId` | La conversación en la que el visitante pidió hablar con una persona |
| `visitorMessage` | El último mensaje del visitante, recortado a 500 caracteres con `…` al final si es más largo |
| `page` | La página donde empezó el chat, o `null` |
| `country` | El código de país del visitante, de dos letras, o `null` |
| `requestedAt` | Cuándo pidió el agente a tu equipo que se uniera, como hora ISO 8601 en UTC |

El evento no indica si alguien se unió. Quién tomó el control del chat, y cuándo, se ve en la [bandeja de Live chat](/es/docs/live-chat#la-bandeja-de-live-chat) y en la transcripción.

### Datos de `return.requested`

| Campo | Contenido |
| --- | --- |
| `returnRequestId` | El ID de la petición en intoCHAT |
| `provider` | La tienda: `shopify` o `woocommerce` |
| `orderName`, `orderId` | El pedido tal como lo muestra tu tienda, por ejemplo `#1042`, y su ID en la tienda |
| `email` | La dirección de correo del pedido. El visitante demostró que la conoce, o la pasó la [verificación de identidad](/es/docs/identity-verification) de tu web. |
| `kind` | `return` o `exchange` |
| `items` | Los artículos, cada uno como `{ "title", "quantity" }`. El título incluye la variante cuando el producto se pidió en varias, por ejemplo `Wool scarf - Blue`. |
| `reason` | El motivo, con las palabras del visitante |
| `status` | `requested` |
| `agentId`, `agentName` | El agente con el que se hizo la petición |
| `conversationId` | La conversación en la que se hizo la petición |
| `createdAt` | Cuándo se guardó la petición, como hora ISO 8601 en UTC |

No se ha aprobado ni cambiado nada en tu tienda. Gestiona la devolución allí y después márcala como resuelta en la lista **Returns** de la pestaña **Leads**.

Los nombres de campo de todos los eventos usan `camelCase`.

### Evento de prueba

**Send test event** envía una petición con `"event": "test"` y estos datos: `{ "message": "This is a test event from intoCHAT. Your webhook is set up correctly." }`. Se envía sean cuales sean los eventos elegidos, también con el webhook en pausa, y solo se intenta una vez. Como sus datos son distintos de los de un evento real, una herramienta que asigna campos a partir de un ejemplo necesita un evento real, por ejemplo un lead que envíes tú mismo desde tu web.

## Comprobar la firma

Comprueba cada petición antes de fiarte de ella. `X-IntoChat-Signature` es `sha256=` seguido del HMAC-SHA256 en hexadecimal de la marca de tiempo, un punto y el cuerpo sin procesar, es decir `${timestamp}.${rawBody}`, con el secreto de firma del webhook como clave. La marca de tiempo es el valor de `X-IntoChat-Timestamp`.

- Calcula la firma sobre el cuerpo exactamente como llega, antes de interpretar el JSON.
- Compara en tiempo constante.
- Rechaza las marcas de tiempo antiguas, para que una petición interceptada no se pueda repetir. El ejemplo de abajo admite 5 minutos.

Este ejemplo de Node.js es el mismo que aparece en **Verify signatures** en la tarjeta **Webhooks**, con un botón **Copy**:

```js
const crypto = require("crypto")

// rawBody: the request body exactly as received, before JSON parsing.
function verifyIntoChatWebhook(rawBody, headers, secret) {
  const timestamp = headers["x-intochat-timestamp"]
  const signature = headers["x-intochat-signature"] || ""

  // Reject anything older than 5 minutes, so a captured request can't be replayed.
  const age = Math.abs(Date.now() / 1000 - Number(timestamp))
  if (!timestamp || !(age <= 300)) return false

  const expected = "sha256=" + crypto
    .createHmac("sha256", secret)
    .update(timestamp + "." + rawBody)
    .digest("hex")

  const a = Buffer.from(signature)
  const b = Buffer.from(expected)
  return a.length === b.length && crypto.timingSafeEqual(a, b)
}
```

Si tu herramienta no puede comprobar firmas, trata la dirección del webhook como una contraseña: cualquiera que la conozca puede enviarle peticiones.

## Responder y evitar duplicados

- Responde con cualquier estado `2xx` en menos de 10 segundos. Todo lo demás cuenta como fallo: otro código de estado, una respuesta que no llega a tiempo o un error de conexión o de certificado.
- Las redirecciones no se siguen. Una respuesta `3xx` cuenta como fallo, así que indica la dirección final.
- El mismo evento puede llegar más de una vez, por ejemplo si tu servidor lo guardó pero respondió demasiado tarde. Usa `X-IntoChat-Delivery`, o el `id` del cuerpo, para ignorar un envío que ya procesaste.
- Para un mismo evento, cada webhook recibe su propio envío, con su propio ID.

## Reintentos

- El primer intento sale justo después del evento. Si falla, siguen hasta dos reintentos rápidos en pocos segundos.
- Si también fallan, el envío muestra **Retrying** y una tarea programada lo vuelve a intentar más tarde. Los reintentos terminan tras 6 intentos en total o 24 horas después del evento, lo que ocurra antes. Después, el envío queda marcado como **Failed**.
- La tarea programada se ejecuta ahora una vez al día. En la práctica, un envío que sigue fallando tras los reintentos rápidos tiene uno o dos intentos más, dentro de aproximadamente un día desde el evento.
- Un envío a una dirección que resulta apuntar a una red privada falla en el acto, sin reintentos.
- Cada reintento conserva el ID del envío y se vuelve a firmar con una marca de tiempo nueva.

## Envíos recientes

Haz clic en **Recent deliveries** debajo de un webhook para ver sus 20 últimos envíos. Cada uno muestra su estado (**Sending**, **Retrying**, **Delivered** o **Failed**), el evento, la hora, el número de intentos y el estado HTTP. Un envío pendiente de reintento muestra cuándo toca el siguiente, y uno que no tuvo éxito muestra el error, por ejemplo `No response within 10 seconds.`

Un envío fallido tiene un botón **Resend** que lo intenta una vez más, al momento. Los envíos se eliminan a los 30 días.

## Siguientes pasos

- Elige qué pide el formulario: [Captación de leads](/es/docs/lead-collection).
- Deja que los visitantes contacten con tu equipo por correo: [Traspaso por correo](/es/docs/handoff).
- Deja que tu equipo se una al chat: [Chat en vivo](/es/docs/live-chat).
- Recoge datos con un formulario bajo la respuesta del agente, y dale a un formulario un webhook propio: [Formularios de chat](/es/docs/chat-forms).
- Deja que los visitantes reserven citas en el chat: [Reservas con Cal.com o Calendly](/es/docs/booking).
- Responde a preguntas sobre pedidos y recoge peticiones de devolución: [Estado de pedidos y devoluciones](/es/docs/orders).
- Publica los leads nuevos, los traspasos y las peticiones de chat en vivo en un canal de Slack: [Avisos en Slack](/es/docs/lead-alerts-and-export#avisos-en-slack).
- Usa intoCHAT en Zapier o Make: [Zapier y Make](/es/docs/zapier-and-make).
- ¿Un envío falla una y otra vez? Consulta [Solución de problemas](/es/docs/troubleshooting).
