Webhooks
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
Endpoint HTTPS preparado para recibir webhooks
Conocimientos básicos de JSON y peticiones HTTP
(Opcional) Herramienta para testing de webhooks (RequestBin, Beeceptor, etc.)
Arquitectura
+------------------+ +-----------------+ +-------------------+
| AsisteClick | | Webhook | | Tu Sistema |
| | | POST | | |
| Conversación | ---> | JSON | ---> | CRM / BI / |
| Archivada | | Payload | | Data Warehouse |
+------------------+ +-----------------+ +-------------------+Flujo de datos:
- Un agente o el sistema archiva una conversación en AsisteClick.
- El backend detecta el cambio de estado a "CLOSED".
- AsisteClick genera un JSON con toda la información de la conversación.
- Se envía un POST HTTP a la URL configurada.
- 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
- Desde el Dashboard, haga clic en el icono de engranaje "Configuración" en el menú lateral.
- 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.

Paso 2: Configurar la URL del Webhook
En la página de Webhooks encontrará:
- Un campo de texto para ingresar su URL de POST.
- El placeholder indica el formato esperado: http://www.site.com/post.html.
Para configurar:
- Ingrese la URL completa de su endpoint (debe ser HTTP o HTTPS).
- Haga clic en el botón "Grabar".

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:
Estructura del Payload JSON
Cada vez que una conversación es archivada, su endpoint recibe un POST con el siguiente payload:
Payload Completo
\{
"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": "[email protected]"
\},
"customer": \{
"fingerprint": "k811xkn31fhzcss0dia8bbd3sr56l20lpx6gx76y",
"name": "John",
"email": "[email protected]",
"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 |
string | Email del agente |
Objeto Customer
Campo | Tipo | Descripción |
|---|---|---|
fingerprint | string | Identificador único del visitante/cliente |
name | string | Nombre proporcionado por el cliente |
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:
https://api.tuempresa.com/webhook::header1:value1;header2:value2Ejemplos:
# 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:asisteclickImportante: Usa :: para separar la URL base de los headers, y ; para separar múltiples headers.
Implementación del Receptor
Ejemplo en Python (Flask)
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)
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
// 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:
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:
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:
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:
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:
- Vaya a webhook.site o requestbin.com
- Copie la URL única que le proporcionan.
- Configúrela en AsisteClick.
- Archive una conversación de prueba.
- Verifique el payload recibido en el sitio de testing.
Verificación Local con ngrok
Si desarrolla localmente:
# Instalar ngrok
npm install -g ngrok
# Exponer su servidor local
ngrok http 3000
# Obtendrá una URL como:
# https://abc123.ngrok.ioUse esa URL en la configuración de AsisteClick.
Logging de Debugging
Agregue logging detallado en su receptor:
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
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/seguridadError: Timeout
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 handlerError: JSON inválido
Causa: El payload tiene caracteres especiales
Solución:
1. Usar json_decode() o JSON.parse() con manejo de errores
2. Verificar encoding UTF-8Lí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
- Responder rápido: Envíe el 200 OK inmediatamente, procese después.
- Cola de procesamiento: Use Redis/RabbitMQ para manejar alto volumen.
- Idempotencia: Guarde ticket_id para evitar duplicados.
- Monitoreo: Implemente alertas si su receptor falla.
Seguridad
Validación de Autenticidad
Verifica que el webhook viene de AsisteClick:
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_tokenMejores Prácticas
- HTTPS obligatorio: Siempre use HTTPS en producción.
- Validar tokens: Verifique los headers de autenticación.
- IP Whitelisting: Si es posible, limite acceso a IPs de AsisteClick.
- Rate limiting: Proteja su endpoint contra abuso.
- 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:
- Acceda a Configuración > Webhooks.
- Borre el contenido del campo URL (déjelo vacío).
- 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:
- Usar una cola (Redis, RabbitMQ, SQS).
- Procesar asincrónicamente.
- Escalar horizontalmente su receptor.
Recursos Adicionales
Herramientas de Testing
- Webhook.site - Inspector de webhooks online.
- RequestBin - Alternativa a webhook.site.
- ngrok - Exponer localhost a internet.
- Postman - Simular requests.
Documentación Relacionada
- Integración con APIs HTTP desde Chatbots.
- Configuración de Canales (WhatsApp, Facebook, etc.).
- Variables de Memoria del Bot.