---
title: Webhooks
slug: webhooks
docTags: 
createdAt: 2026-01-08T20:11:12.255Z
---

# Conectar AsisteClick con Cualquier Sistema Externo

**Tiempo:** 20-30 minutos | **Nivel:** Técnico | **Requiere:** Administrador o Super Administrador

***

## Resumen

Este tutorial enseña a configurar webhooks para recibir automáticamente una copia de cada conversación archivada en formato JSON. Esta funcionalidad permite integrar AsisteClick con CRMs propios, sistemas de BI, data warehouses, herramientas de analytics o cualquier sistema que acepte llamadas HTTP POST.

***

## Antes de Empezar

- [ ] Acceso como administrador&#x20;
- [ ] Endpoint HTTPS preparado para recibir webhooks
- [ ] Conocimientos básicos de JSON y peticiones HTTP
- [ ] (Opcional) Herramienta para testing de webhooks (RequestBin, Beeceptor, etc.)

***

## Arquitectura

```text
+------------------+      +-----------------+      +-------------------+
|   AsisteClick    |      |    Webhook      |      |   Tu Sistema      |
|                  |      |    POST         |      |                   |
|  Conversación     | ---> |    JSON         | ---> |   CRM / BI /      |
|  Archivada       |      |    Payload      |      |   Data Warehouse  |
+------------------+      +-----------------+      +-------------------+
```

**Flujo de datos:**

1. Un agente o el sistema archiva una conversación en AsisteClick.
2. El backend detecta el cambio de estado a "CLOSED".
3. AsisteClick genera un JSON con toda la información de la conversación.
4. Se envía un POST HTTP a la URL configurada.
5. Tu sistema recibe y procesa los datos.

***

## Eventos que Disparan Webhooks

El sistema de webhooks se activa automáticamente en los siguientes eventos:

| Evento           | Descripción            | Cuando Ocurre                                                 |
| ---------------- | ---------------------- | ------------------------------------------------------------- |
| `TICKET.CLOSED`  | Conversación archivada | Agente cierra el chat o el sistema lo archiva automáticamente |
| `TICKET.HANDOFF` | Transferencia a agente | Bot transfiere a humano o ticket se abre                      |
| `EVENT.CREATED`  | Evento creado          | Se agenda una cita desde el chatbot                           |
| `EVENT.UPDATED`  | Evento modificado      | Se modifica una cita existente                                |
| `EVENT.CANCELED` | Evento cancelado       | Se cancela una cita agendada                                  |

***

## Configuración Inicial

### Paso 1: Acceder a la Configuración de Webhooks

1. Desde el Dashboard, haga clic en el icono de engranaje **"Configuración"** en el menú lateral.
2. En el panel que se despliega a la derecha, haga clic en **"Webhooks"**.

**Nota:** Esta opción solo es visible para usuarios con rol de administrador o super administrador.

::Image[]{src="https://api.archbee.com/api/optimize/1084p5usHR6NhBhJbNXtr/oD3JinVvTOeJot5yL6kyf-20260109-035612.png" size="26" width="259" height="538" position="center" darkWidth="259" darkHeight="538" showCaption="false"}

### Paso 2: Configurar la URL del Webhook

En la página de Webhooks encontrará:

1. Un campo de texto para ingresar su URL de POST.
2. El placeholder indica el formato esperado: `http://www.site.com/post.html`.

**Para configurar:**

1. Ingrese la URL completa de su endpoint (debe ser HTTP o HTTPS).
2. Haga clic en el botón **"Grabar"**.

![](https://api.archbee.com/api/optimize/1084p5usHR6NhBhJbNXtr/X8a1W_JwUmJj20U9Keupw-20260109-035650.png)

### Paso 3: Validación de URL

El sistema valida que la URL cumpla con el formato correcto:

**Formatos válidos:**

- `https://api.tuempresa.com/webhooks/asisteclick`
- `http://192.168.1.100:3000/webhook`
- `https://webhook.site/abc-123-def`

**Formatos inválidos:**

- URLs sin protocolo: `api.ejemplo.com/webhook`
- Protocolos no soportados: `ftp://servidor.com`
- URLs mal formadas.

**Mensaje de error:** Si la URL es inválida, verá el mensaje:

:::BlockQuote
"La URL \[tu-url] no existe. Por favor, ingrese una URL válida (por ejemplo [http://www.sitio.com/pagina.php](http://www.sitio.com/pagina.php))"
:::

***

## Estructura del Payload JSON

Cada vez que una conversación es archivada, su endpoint recibe un POST con el siguiente payload:

### Payload Completo

```json
\{
  "event_type": "TICKET.CLOSED",
  "ticket_id": "#1FY",
  "key": "md5(ticket_id + access_token)",
  "timestamp": 1586229907754,
  "channel": "WEB",
  "source": "CHAT",
  "source_id": null,
  "status": "CLOSED",
  "subject": "I'm interested in the new red shoes",
  "department": \{
    "id": 93,
    "name": "Customer support"
  \},
  "bot": \{
    "id": 25478,
    "name": "Agustin",
    "transferred_to_agent": true
  \},
  "agent": \{
    "id": 569884,
    "name": "Agustin",
    "email": "agustin@hello.com"
  \},
  "customer": \{
    "fingerprint": "k811xkn31fhzcss0dia8bbd3sr56l20lpx6gx76y",
    "name": "John",
    "email": "john@gmail.com",
    "phone": "",
    "facebook_id": "",
    "sentiment": "",
    "ip": "95.256.142.145",
    "browser_os": "Chrome on Windows 10 64-bit",
    "country_code": "US",
    "country_name": "United States"
  \},
  "event": \{
    "calendar_id": "1",
    "base36_id": "HX34S",
    "customer_locale": "es",
    "customer_timezone": "-3",
    "event_status": "ACTIVE",
    "event_url": "<event url>",
    "friendly": "Chrome on Windows 10 64-bit",
    "location": "Av. Libertador 2632, Olivos"
  \},
  "messages": [
    \{
      "action": "OUT",
      "timestamp": 1586229888461,
      "name": "Bruno",
      "message": "Hi John! I'm Bruno, a virtual assistant. How can I help you?"
    \},
    \{
      "action": "IN",
      "timestamp": 1586229891562,
      "name": "John",
      "message": "I'm interested in the new red shoes"
    \}
  ],
  "tags": ["venta", "calzado"],
  "custom_fields": [
    \{
      "producto_interes": "zapatos rojos"
    \}
  ]
\}
```

***

## Descripción de Campos

### Metadatos del Ticket

| Campo        | Tipo        | Descripción                                                                                           |
| ------------ | ----------- | ----------------------------------------------------------------------------------------------------- |
| `event_type` | string      | Tipo de evento: `TICKET.CLOSED`, `TICKET.HANDOFF`, `EVENT.CREATED`, `EVENT.UPDATED`, `EVENT.CANCELED` |
| `ticket_id`  | string      | Identificador único del ticket en formato Base36 (ej: `#1FY`)                                         |
| `key`        | string      | Hash MD5 para validación de autenticidad                                                              |
| `timestamp`  | number      | Timestamp Unix en milisegundos del momento de archivado                                               |
| `channel`    | string      | Canal de origen: `WEB`, `WHATSAPP`, `FACEBOOK`, `INSTAGRAM`, `TELEGRAM`, `TEAMS`                      |
| `source`     | string      | Origen: `CHAT`, `BOT`, `EMAIL`                                                                        |
| `source_id`  | string/null | ID adicional del origen (si aplica)                                                                   |
| `status`     | string      | Estado: `CLOSED`, `OPEN`, `PENDING`                                                                   |
| `subject`    | string      | Asunto o primer mensaje de la conversación                                                            |

### Objeto Department

| Campo  | Tipo   | Descripción                  |
| ------ | ------ | ---------------------------- |
| `id`   | number | ID numérico del departamento |
| `name` | string | Nombre del departamento      |

### Objeto Bot

| Campo                  | Tipo    | Descripción                                |
| ---------------------- | ------- | ------------------------------------------ |
| `id`                   | number  | ID del chatbot que atendió inicialmente    |
| `name`                 | string  | Nombre del chatbot                         |
| `transferred_to_agent` | boolean | `true` si se transfirió a un agente humano |

### Objeto Agent

| Campo   | Tipo   | Descripción                           |
| ------- | ------ | ------------------------------------- |
| `id`    | number | ID del agente que atendió (si aplica) |
| `name`  | string | Nombre del agente                     |
| `email` | string | Email del agente                      |

### Objeto Customer

| Campo          | Tipo   | Descripción                                                        |
| -------------- | ------ | ------------------------------------------------------------------ |
| `fingerprint`  | string | Identificador único del visitante/cliente                          |
| `name`         | string | Nombre proporcionado por el cliente                                |
| `email`        | string | Email del cliente                                                  |
| `phone`        | string | Teléfono (si lo proporcionó)                                       |
| `facebook_id`  | string | ID de Facebook (para canal Facebook Messenger)                     |
| `sentiment`    | string | Análisis de sentimiento: `positive`, `negative`, `neutral` o vacío |
| `ip`           | string | Dirección IP del cliente                                           |
| `browser_os`   | string | Navegador y sistema operativo detectados                           |
| `country_code` | string | Código de país ISO (ej: `US`, `AR`, `MX`)                          |
| `country_name` | string | Nombre del país                                                    |

### Objeto Event (Citas/Agendamiento)

Solo presente cuando la conversación incluye una cita agendada:

| Campo               | Tipo   | Descripción                               |
| ------------------- | ------ | ----------------------------------------- |
| `calendar_id`       | string | ID de la agenda utilizada                 |
| `base36_id`         | string | ID de la cita en Base36                   |
| `customer_locale`   | string | Idioma del cliente (ej: `es`, `en`)       |
| `customer_timezone` | string | Zona horaria UTC (ej: `-3`, `+1`)         |
| `event_status`      | string | Estado: `ACTIVE`, `CANCELED`, `COMPLETED` |
| `event_url`         | string | URL de la página del evento               |
| `friendly`          | string | Descripción amigable del dispositivo      |
| `location`          | string | Ubicación de la cita                      |

### Array Messages

Historial completo de la conversación:

| Campo       | Tipo   | Descripción                                                             |
| ----------- | ------ | ----------------------------------------------------------------------- |
| `action`    | string | Dirección: `IN` (cliente), `OUT` (agente/bot), `PRIVATE` (nota interna) |
| `timestamp` | number | Timestamp Unix en milisegundos                                          |
| `name`      | string | Nombre del emisor                                                       |
| `message`   | string | Contenido del mensaje                                                   |

### Arrays Adicionales

| Campo           | Tipo  | Descripción                                     |
| --------------- | ----- | ----------------------------------------------- |
| `tags`          | array | Etiquetas asignadas a la conversación           |
| `custom_fields` | array | Campos personalizados capturados por el chatbot |

***

## Headers HTTP Enviados

Cada webhook incluye headers de autenticación:

| Header                     | Descripción                         |
| -------------------------- | ----------------------------------- |
| `Content-Type`             | `application/json`                  |
| `AsisteClick-Ticket-Token` | Hash MD5 del ticket para validación |
| `AsisteClick-Access-Token` | Token de acceso de la cuenta        |

***

## Configuración de Headers Personalizados

Puedes agregar headers personalizados a tus webhooks usando el formato especial en la URL:

**Formato:**

```text
https://api.tuempresa.com/webhook::header1:value1;header2:value2
```

**Ejemplos:**

```text
# Agregar API Key
https://api.tuempresa.com/webhook::x-api-key:tu-api-key-secreto

# Agregar múltiples headers
https://api.tuempresa.com/webhook::Authorization:Bearer abc123;X-Custom-Header:valor

# Agregar header de ambiente
https://api.tuempresa.com/webhook::Environment:production;Source:asisteclick
```

**Importante:** Usa `::` para separar la URL base de los headers, y `;` para separar múltiples headers.

***

## Implementación del Receptor

### Ejemplo en Python (Flask)

```python
from flask import Flask, request, jsonify
import hashlib
import json

app = Flask(__name__)

# Tu clave privada (debe coincidir con AsisteClick)
PRIVATE_KEY = "tu-clave-secreta"

def validate_ticket_token(ticket_id, received_token):
    """Valida el token del ticket"""
    expected = hashlib.md5(
        (ticket_id + PRIVATE_KEY).encode()
    ).hexdigest()
    return received_token == expected

@app.route('/webhook/asisteclick', methods=['POST'])
def webhook():
    # Obtener headers de autenticacion
    ticket_token = request.headers.get('AsisteClick-Ticket-Token')
    access_token = request.headers.get('AsisteClick-Access-Token')

    # Parsear el payload
    data = request.get_json()

    # Validar autenticacion (opcional pero recomendado)
    if not ticket_token or not access_token:
        return jsonify(\{'error': 'Missing authentication'\}), 401

    # Procesar según tipo de evento
    event_type = data.get('event_type')
    ticket_id = data.get('ticket_id')

    print(f"Recibido: \{event_type\} - Ticket: \{ticket_id\}")

    if event_type == 'TICKET.CLOSED':
        process_closed_ticket(data)
    elif event_type == 'TICKET.HANDOFF':
        process_handoff(data)
    elif event_type.startswith('EVENT.'):
        process_calendar_event(data)

    # Responder con 200 OK
    return jsonify(\{'status': 'received'\}), 200

def process_closed_ticket(data):
    """Procesa tickets cerrados"""
    customer = data.get('customer', \{\})
    messages = data.get('messages', [])

    # Ejemplo: Guardar en base de datos
    # db.save_conversation(data)

    # Ejemplo: Enviar a CRM
    # crm.create_interaction(customer['email'], messages)

    print(f"Ticket cerrado - Cliente: \{customer.get('name')\}")
    print(f"Total mensajes: \{len(messages)\}")

def process_handoff(data):
    """Procesa transferencias a agente"""
    agent = data.get('agent', \{\})
    print(f"Transferido a: \{agent.get('name')\}")

def process_calendar_event(data):
    """Procesa eventos de calendario"""
    event = data.get('event', \{\})
    print(f"Evento: \{event.get('event_status')\}")

if __name__ == '__main__':
    app.run(port=5000)
```

### Ejemplo en Node.js (Express)

```javascript
const express = require('express');
const crypto = require('crypto');

const app = express();
app.use(express.json());

const PRIVATE_KEY = 'tu-clave-secreta';

// Función para validar token
function validateToken(ticketId, receivedToken) \{
    const expected = crypto
        .createHash('md5')
        .update(ticketId + PRIVATE_KEY)
        .digest('hex');
    return expected === receivedToken;
\}

// Endpoint del webhook
app.post('/webhook/asisteclick', (req, res) => \{
    // Headers de autenticación
    const ticketToken = req.headers['asisteclick-ticket-token'];
    const accessToken = req.headers['asisteclick-access-token'];

    // Payload
    const data = req.body;
    const eventType = data.event_type;
    const ticketId = data.ticket_id;

    console.log(`Evento: $\{eventType\} | Ticket: $\{ticketId\}`);

    // Procesar según tipo de evento
    switch (eventType) \{
        case 'TICKET.CLOSED':
            handleClosedTicket(data);
            break;
        case 'TICKET.HANDOFF':
            handleHandoff(data);
            break;
        default:
            if (eventType.startsWith('EVENT.')) \{
                handleCalendarEvent(data);
            \}
    \}

    // Responder OK
    res.status(200).json(\{ status: 'received' \});
\});

function handleClosedTicket(data) \{
    const customer = data.customer || \{\};
    const messages = data.messages || [];

    console.log(`Cliente: $\{customer.name || 'Desconocido'\}`);
    console.log(`Email: $\{customer.email || 'No disponible'\}`);
    console.log(`Canal: $\{data.channel\}`);
    console.log(`Mensajes: $\{messages.length\}`);

    // Ejemplo: Guardar en MongoDB
    // await db.conversations.insertOne(data);

    // Ejemplo: Enviar a Slack
    // await slack.sendMessage(formatConversation(data));
\}

function handleHandoff(data) \{
    const agent = data.agent || \{\};
    console.log(`Transferido a: $\{agent.name\}`);
\}

function handleCalendarEvent(data) \{
    const event = data.event || \{\};
    console.log(`Evento calendario: $\{event.event_status\}`);
\}

app.listen(3000, () => \{
    console.log('Webhook server running on port 3000');
\});
```

### Ejemplo en PHP

```php
<?php
// webhook.php

// Leer el payload JSON
$input = file_get_contents('php://input');
$data = json_decode($input, true);

// Headers de autenticación
$ticketToken = $_SERVER['HTTP_ASISTECLICK_TICKET_TOKEN'] ?? null;
$accessToken = $_SERVER['HTTP_ASISTECLICK_ACCESS_TOKEN'] ?? null;

// Log del evento
error_log("Webhook recibido: " . $data['event_type'] . " - " . $data['ticket_id']);

// Validar que llegó data
if (!$data || !isset($data['event_type'])) \{
    http_response_code(400);
    echo json_encode(['error' => 'Invalid payload']);
    exit;
\}

// Procesar según tipo de evento
switch ($data['event_type']) \{
    case 'TICKET.CLOSED':
        processClosedTicket($data);
        break;
    case 'TICKET.HANDOFF':
        processHandoff($data);
        break;
    default:
        if (strpos($data['event_type'], 'EVENT.') === 0) \{
            processCalendarEvent($data);
        \}
\}

// Responder OK
http_response_code(200);
echo json_encode(['status' => 'received']);

function processClosedTicket($data) \{
    $customer = $data['customer'] ?? [];
    $messages = $data['messages'] ?? [];

    // Ejemplo: Guardar en MySQL
    // $pdo->prepare("INSERT INTO conversations ...")->execute([...]);

    // Ejemplo: Enviar a API de CRM
    // $ch = curl_init('https://tu-crm.com/api/interactions');

    error_log("Ticket cerrado: " . ($customer['name'] ?? 'N/A'));
    error_log("Canal: " . ($data['channel'] ?? 'N/A'));
    error_log("Mensajes: " . count($messages));
\}

function processHandoff($data) \{
    $agent = $data['agent'] ?? [];
    error_log("Transferido a: " . ($agent['name'] ?? 'N/A'));
\}

function processCalendarEvent($data) \{
    $event = $data['event'] ?? [];
    error_log("Evento: " . ($event['event_status'] ?? 'N/A'));
\}
?>
```

***

## Casos de Uso Comunes

### 1. Sincronización con CRM

Guarda automáticamente cada conversación como interacción en tu CRM:

```python
def sync_to_crm(data):
    customer = data['customer']

    # Buscar o crear contacto
    contact = crm.find_by_email(customer['email'])
    if not contact:
        contact = crm.create_contact(\{
            'name': customer['name'],
            'email': customer['email'],
            'phone': customer['phone'],
            'source': 'AsisteClick'
        \})

    # Crear interacción/nota
    crm.add_interaction(contact['id'], \{
        'type': 'chat',
        'channel': data['channel'],
        'subject': data['subject'],
        'transcript': format_messages(data['messages']),
        'agent': data['agent']['name'] if data.get('agent') else 'Bot',
        'tags': data.get('tags', [])
    \})
```

### 2. Analytics y Business Intelligence

Envía datos a tu data warehouse para análisis:

```javascript
async function sendToBigQuery(data) \{
    const row = \{
        ticket_id: data.ticket_id,
        timestamp: new Date(data.timestamp),
        channel: data.channel,
        customer_country: data.customer.country_code,
        customer_email: data.customer.email,
        message_count: data.messages.length,
        had_agent: data.bot.transferred_to_agent,
        agent_id: data.agent?.id || null,
        department: data.department?.name,
        tags: data.tags?.join(','),
        sentiment: data.customer.sentiment
    \};

    await bigquery
        .dataset('asisteclick')
        .table('conversations')
        .insert([row]);
\}
```

### 3. Alertas en Slack/Teams

Notifica conversaciones importantes:

```python
def notify_slack(data):
    customer = data['customer']

    # Solo notificar si tiene etiqueta importante
    if 'urgente' in data.get('tags', []):
        slack_message = \{
            'text': f":rotating_light: Conversación urgente cerrada",
            'blocks': [
                \{
                    'type': 'section',
                    'text': \{
                        'type': 'mrkdwn',
                        'text': f"*Cliente:* \{customer['name']\}\n*Email:* \{customer['email']\}\n*Canal:* \{data['channel']\}"
                    \}
                \}
            ]
        \}
        requests.post(SLACK_WEBHOOK_URL, json=slack_message)
```

### 4. Backup a Amazon S3

Archiva todas las conversaciones en S3:

```javascript
const AWS = require('aws-sdk');
const s3 = new AWS.S3();

async function backupToS3(data) \{
    const key = `conversations/$\{data.channel\}/$\{data.ticket_id\}.json`;

    await s3.putObject(\{
        Bucket: 'asisteclick-backup',
        Key: key,
        Body: JSON.stringify(data, null, 2),
        ContentType: 'application/json'
    \}).promise();

    console.log(`Guardado en S3: $\{key\}`);
\}
```

***

## Testing y Debugging

### Usando RequestBin o Webhook.site

Para probar sin implementar un servidor:

1. Vaya a [webhook.site](https://webhook.site) o [requestbin.com](https://requestbin.com)
2. Copie la URL única que le proporcionan.
3. Configúrela en AsisteClick.
4. Archive una conversación de prueba.
5. Verifique el payload recibido en el sitio de testing.

### Verificación Local con ngrok

Si desarrolla localmente:

```bash
# Instalar ngrok
npm install -g ngrok

# Exponer su servidor local
ngrok http 3000

# Obtendrá una URL como:
# https://abc123.ngrok.io
```

Use esa URL en la configuración de AsisteClick.

### Logging de Debugging

Agregue logging detallado en su receptor:

```python
import json
from datetime import datetime

def log_webhook(data):
    log_entry = \{
        'received_at': datetime.now().isoformat(),
        'event_type': data.get('event_type'),
        'ticket_id': data.get('ticket_id'),
        'channel': data.get('channel'),
        'payload_size': len(json.dumps(data))
    \}

    with open('webhook_log.jsonl', 'a') as f:
        f.write(json.dumps(log_entry) + '\n')
```

***

## Manejo de Errores

### Respuestas HTTP Esperadas

| Código  | Significado    | Acción de AsisteClick               |
| ------- | -------------- | ----------------------------------- |
| 200-299 | Éxito          | Webhook entregado correctamente     |
| 400     | Bad Request    | Error en el payload - revisar logs  |
| 401/403 | No autorizado  | Verificar headers de autenticación  |
| 500+    | Error servidor | Error en su endpoint - revisar logs |

### Errores Comunes

**Error: Webhook no llega**

```text
Causa: URL mal configurada o servidor no accesible
Solución:
1. Verificar que la URL sea accesible públicamente
2. Confirmar que el puerto está abierto
3. Revisar firewall/seguridad
```

**Error: Timeout**

```text
Causa: Su servidor tarda mucho en responder
Solución:
1. Responder inmediatamente con 200 OK
2. Procesar el payload de forma asíncrona
3. No hacer operaciones pesadas en el request handler
```

**Error: JSON inválido**

```text
Causa: El payload tiene caracteres especiales
Solución:
1. Usar json_decode() o JSON.parse() con manejo de errores
2. Verificar encoding UTF-8
```

***

## Límites y Consideraciones

| Límite              | Valor               | Nota                                               |
| ------------------- | ------------------- | -------------------------------------------------- |
| Timeout del webhook | 30 segundos         | Responde rápido o el request fallará               |
| Reintentos          | No automático       | Actualmente no hay cola de reintentos              |
| Payload máximo      | Sin límite definido | Depende del tamaño de la conversación              |
| Concurrencia        | Asíncrono           | Múltiples webhooks pueden enviarse simultáneamente |

### Recomendaciones de Performance

1. **Responder rápido**: Envíe el 200 OK inmediatamente, procese después.
2. **Cola de procesamiento**: Use Redis/RabbitMQ para manejar alto volumen.
3. **Idempotencia**: Guarde `ticket_id` para evitar duplicados.
4. **Monitoreo**: Implemente alertas si su receptor falla.

***

## Seguridad

### Validación de Autenticidad

Verifica que el webhook viene de AsisteClick:

```python
import hashlib

def verify_webhook(ticket_id, received_token, access_token):
    # Generar token esperado
    private_key = "asdfsafeWEREereafa(*@sfad5869)_12342aaaaaa!#232AASF!"
    expected_ticket_token = hashlib.md5(
        (ticket_id + private_key).encode()
    ).hexdigest()

    # Validar key del payload
    expected_key = hashlib.md5(
        (ticket_id + access_token).encode()
    ).hexdigest()

    return received_token == expected_ticket_token
```

### Mejores Prácticas

1. **HTTPS obligatorio**: Siempre use HTTPS en producción.
2. **Validar tokens**: Verifique los headers de autenticación.
3. **IP Whitelisting**: Si es posible, limite acceso a IPs de AsisteClick.
4. **Rate limiting**: Proteja su endpoint contra abuso.
5. **Logs de auditoría**: Registre todos los webhooks recibidos.

***

## Troubleshooting

| Síntoma            | Causa Probable         | Solución                                 |
| ------------------ | ---------------------- | ---------------------------------------- |
| No llegan webhooks | URL inválida o vacía   | Verificar configuración en dashboard     |
| Error 401/403      | Falta autenticación    | Revisar headers requeridos               |
| Payload vacío      | Error de parsing       | Leer `php://input` o `request.body`      |
| Datos incompletos  | Conversación sin datos | Algunos campos son opcionales            |
| Duplicados         | Reintentos manuales    | Implementar idempotencia por `ticket_id` |
| Latencia alta      | Procesamiento síncrono | Usar colas asíncronas                    |

***

## Deshabilitar Webhooks

Para deshabilitar el envío de webhooks:

1. Acceda a **Configuración > Webhooks**.
2. Borre el contenido del campo URL (déjelo vacío).
3. Haga clic en **"Grabar"**.

Verá el mensaje de confirmación "Datos grabados!" y los webhooks dejarán de enviarse.

***

## Preguntas Frecuentes

**¿Puedo configurar múltiples URLs de webhook?**

Actualmente solo se soporta una URL por cuenta. Si necesita enviar a múltiples destinos, implemente un router en su servidor que redistribuya a los destinos finales.

**¿Qué pasa si mi servidor está caído?**

El sistema no tiene cola de reintentos automática. Los webhooks que fallen durante una caída se perderán. Recomendamos:

- Alta disponibilidad en su receptor.
- Monitoreo de uptime.
- Logs para identificar webhooks perdidos.

**¿Los webhooks incluyen archivos adjuntos?**

Los mensajes con archivos incluyen la URL del archivo en el campo `message`. El archivo en sí no se envía en el payload, pero puede descargarlo desde la URL proporcionada.

**¿Puedo filtrar qué conversaciones enviar?**

Actualmente se envían todas las conversaciones archivadas. El filtrado debe hacerse en su receptor basándose en campos como `channel`, `department`, `tags`, etc.

**¿Cómo manejo gran volumen de webhooks?**

Para alto volumen recomendamos:

1. Usar una cola (Redis, RabbitMQ, SQS).
2. Procesar asincrónicamente.
3. Escalar horizontalmente su receptor.

***

## Recursos Adicionales

### Herramientas de Testing

- [Webhook.site](https://webhook.site) - Inspector de webhooks online.
- [RequestBin](https://requestbin.com) - Alternativa a webhook.site.
- [ngrok](https://ngrok.com) - Exponer localhost a internet.
- [Postman](https://postman.com) - Simular requests.

### Documentación Relacionada

- Integración con APIs HTTP desde Chatbots.
- Configuración de Canales (WhatsApp, Facebook, etc.).
- Variables de Memoria del Bot.

