Endpoint y callback para turnero integrado con sistema externo
Nivel: Tecnico | Requiere: Plan Company o superior, calendario configurado, acceso admin o super
Resumen
Este tutorial te permite conectar el skill de turnero (agendamiento) de AsisteClick con tu propio sistema de gestion de citas. Configuraras dos endpoints:
- API de Disponibilidad: AsisteClick consulta tu sistema para obtener los horarios disponibles
- API de Callback: Tu sistema recibe notificaciones cuando se crea, modifica o cancela una reserva
Con esta integracion, el chatbot actua como interfaz conversacional mientras tu sistema externo mantiene el control total de la agenda.
Antes de Empezar
Servidor o servicio que pueda recibir peticiones HTTP POST
Endpoint REST que devuelva JSON con horarios disponibles
Plan Company o superior en AsisteClick
Un calendario creado en AsisteClick (ve a Configuracion > Agendas)
Conocimientos basicos de APIs REST y formato JSON
Acceso de administrador o super en AsisteClick
Arquitectura
+-------------------+ +------------------+ +------------------+
| | 1. POST | | 2. POST | |
| AsisteClick | -------> | Tu Endpoint | -------> | Tu Sistema |
| (Chatbot) | slots? | Disponibilidad | reserva | de Turnos |
| | <------- | | <------- | |
+-------------------+ JSON +------------------+ +------------------+
| |
| 3. POST (callback) |
| <----------------------------------------------------------|
| EVENT.CREATED / EVENT.UPDATED / EVENT.CANCELED |
+------------------------------------------------------------+Flujo completo:
- El cliente solicita un turno via chatbot
- AsisteClick llama a tu API de Disponibilidad con los slots nativos
- Tu sistema responde con los slots filtrados/disponibles
- El cliente selecciona un horario
- AsisteClick crea la reserva y llama a tu API de Callback
- Tu sistema procesa la nueva reserva
Configuracion Inicial
Paso 1: Accede a la configuracion del calendario
- En el menu lateral, ve a Configuracion > Agendas
- Selecciona el calendario que quieres integrar (o crea uno nuevo)
- Desplazate hasta la seccion de integracion API
Paso 2: Habilita el modo API
Activa la opcion "Mostrar disponibilidad por API".
Esto le indica a AsisteClick que en lugar de usar su motor interno de disponibilidad, debe consultar tu endpoint externo.
Paso 3: Configura el API Endpoint (Disponibilidad)
En el campo "API Endpoint", ingresa la URL de tu servicio que devuelve horarios disponibles:
https://tu-dominio.com/api/turnero/disponibilidadEste endpoint recibira un POST con los slots nativos generados por AsisteClick y debera responder con los slots que estan realmente disponibles en tu sistema.
Paso 4: Configura el Callback API Endpoint (Notificaciones)
En el campo "Callback API Endpoint", ingresa la URL donde AsisteClick enviara las notificaciones de eventos:
https://tu-dominio.com/api/turnero/callbackEste endpoint recibira un POST cada vez que se cree, modifique o cancele una reserva.
Paso 5: Guarda la configuracion
Haz clic en "Guardar" para aplicar los cambios.
API de Disponibilidad
Request que recibiras
Metodo: POST Content-Type: application/json User-Agent: AsisteClick-Booking/2.0
AsisteClick enviara el siguiente payload a tu endpoint:
{
"calendar_id": 5,
"timezone": "-3",
"preferred": "2025-01-15T10:00:00",
"count": 6,
"start_date": "2025-01-15",
"end_date": "2025-01-22",
"slots": [
{
"text": "Miercoles 15 de Enero - 09:00",
"isoLocal": "2025-01-15T09:00:00",
"iso": "2025-01-15T12:00:00Z",
"id": "slot_001"
},
{
"text": "Miercoles 15 de Enero - 10:00",
"isoLocal": "2025-01-15T10:00:00",
"iso": "2025-01-15T13:00:00Z",
"id": "slot_002"
},
{
"text": "Miercoles 15 de Enero - 11:00",
"isoLocal": "2025-01-15T11:00:00",
"iso": "2025-01-15T14:00:00Z",
"id": "slot_003"
}
]
}Descripcion de campos del request
Campo | Tipo | Descripcion |
|---|---|---|
calendar_id | number | ID del calendario en AsisteClick |
timezone | string | Offset de zona horaria del calendario (ej: "-3" para Argentina) |
preferred | string/null | Fecha/hora preferida por el cliente (ISO 8601), si la indico |
count | number | Cantidad maxima de slots a mostrar al cliente |
start_date | string | Fecha de inicio del rango de busqueda (YYYY-MM-DD) |
end_date | string | Fecha de fin del rango de busqueda (YYYY-MM-DD) |
slots | array | Slots nativos pre-generados por AsisteClick segun la configuracion del calendario |
Descripcion de cada slot
Campo | Tipo | Descripcion |
|---|---|---|
text | string | Texto legible para mostrar al cliente |
isoLocal | string | Fecha/hora en zona horaria local (ISO 8601 sin offset) |
iso | string | Fecha/hora en UTC (ISO 8601 con Z) |
id | string | Identificador unico del slot |
Response que debes devolver
Tu endpoint debe responder con los slots disponibles en tu sistema:
Status Code: 200 OK Content-Type: application/json
{
"slots": [
{
"text": "Miercoles 15 de Enero - 10:00",
"isoLocal": "2025-01-15T10:00:00",
"iso": "2025-01-15T13:00:00Z",
"id": "slot_002"
},
{
"text": "Miercoles 15 de Enero - 11:00",
"isoLocal": "2025-01-15T11:00:00",
"iso": "2025-01-15T14:00:00Z",
"id": "slot_003"
}
],
"showBeforeButton": false,
"showAfterButton": true
}Descripcion de campos del response
Campo | Tipo | Requerido | Descripcion |
|---|---|---|---|
slots | array | Si | Array de slots disponibles (misma estructura que el request) |
showBeforeButton | boolean | No | Si mostrar boton "Anterior" para navegar a fechas previas |
showAfterButton | boolean | No | Si mostrar boton "Siguiente" para navegar a fechas posteriores |
Logica de filtrado recomendada
Tu endpoint deberia:
- Recibir los slots pre-generados por AsisteClick
- Consultar tu base de datos para verificar disponibilidad
- Filtrar los slots que ya estan ocupados
- Devolver solo los slots disponibles
// Pseudocodigo - Ejemplo en Node.js
app.post('/api/turnero/disponibilidad', async (req, res) => {
const { calendar_id, slots, start_date, end_date } = req.body;
// Obtener reservas existentes en tu sistema
const reservasExistentes = await db.query(`
SELECT fecha_hora FROM reservas
WHERE fecha_hora BETWEEN ? AND ?
AND estado = 'confirmada'
`, [start_date, end_date]);
// Convertir a Set para busqueda rapida
const horariosOcupados = new Set(
reservasExistentes.map(r => r.fecha_hora.toISOString())
);
// Filtrar slots disponibles
const slotsDisponibles = slots.filter(slot =>
!horariosOcupados.has(slot.iso)
);
res.json({
slots: slotsDisponibles,
showBeforeButton: false,
showAfterButton: slotsDisponibles.length > 0
});
});Errores y fallback
Si tu endpoint falla o no responde a tiempo, AsisteClick tiene un mecanismo de fallback:
Situacion | Comportamiento |
|---|---|
Timeout (>5 segundos) | Usa motor nativo de AsisteClick |
Status code >= 500 | Reintenta hasta 2 veces, luego fallback |
Status code 4xx | Usa motor nativo de AsisteClick |
Response invalido | Usa motor nativo de AsisteClick |
Esto garantiza que el cliente siempre reciba una respuesta, aunque tu sistema este temporalmente no disponible.
API de Callback (Notificaciones)
Cuando se envia el callback
AsisteClick enviara un POST a tu callback endpoint cuando ocurra cualquiera de estos eventos:
Evento | Descripcion |
|---|---|
EVENT.CREATED | Se creo una nueva reserva |
EVENT.UPDATED | Se modifico una reserva existente (reagendamiento) |
EVENT.CANCELED | Se cancelo una reserva |
Request que recibiras
Metodo: POST Content-Type: application/json
Headers adicionales:
Header | Descripcion |
|---|---|
AsisteClick-Ticket-Token | Token unico de verificacion del ticket |
AsisteClick-Access-Token | Token de acceso de la cuenta |
Payload completo:
{
"event_type": "EVENT.CREATED",
"ticket_id": "#A1B2C3",
"key": "d41d8cd98f00b204e9800998ecf8427e",
"timestamp": "2025-01-15T10:30:00Z",
"channel": "WHATSAPP",
"source": "whatsapp_api",
"source_id": "+5491155551234",
"status": "OPEN",
"subject": "Reserva de turno",
"bot": {
"id": 123,
"name": "Bot de Turnos"
},
"agent": {
"id": null,
"name": null,
"email": null
},
"department": {
"id": 5,
"name": "Atencion al Cliente"
},
"customer": {
"fingerprint": "abc123xyz",
"name": "Juan Perez",
"email": "[email protected]",
"phone": "+5491155551234",
"facebook_id": null,
"sentiment": null,
"ip": "190.100.50.25",
"browser_os": "WhatsApp/2.23.5",
"country_code": "AR",
"country_name": "Argentina"
},
"event": {
"id": 456,
"calendar_id": 5,
"calendar_title": "Turnos Consulta",
"utc_from": "2025-01-15T13:00:00Z",
"utc_to": "2025-01-15T13:30:00Z",
"location": "Consultorio 3, Piso 2",
"public_key": "XY7K9M2P"
},
"messages": [
{
"action": "IN",
"timestamp": "2025-01-15T10:25:00Z",
"name": "Juan Perez",
"message": "Hola, quiero sacar un turno",
"attachments": null
},
{
"action": "OUT",
"timestamp": "2025-01-15T10:25:05Z",
"name": "Bot de Turnos",
"message": "Perfecto! Estos son los horarios disponibles...",
"attachments": null
}
],
"tags": ["turno", "consulta_general"],
"custom_fields": [
{
"id": "motivo_consulta",
"value": "Control anual"
},
{
"id": "obra_social",
"value": "OSDE"
}
]
}Descripcion de campos principales
Campo | Tipo | Descripcion |
|---|---|---|
event_type | string | Tipo de evento: EVENT.CREATED, EVENT.UPDATED, EVENT.CANCELED |
ticket_id | string | ID del ticket en formato base36 con prefijo # |
key | string | Hash MD5 para verificacion de autenticidad |
timestamp | string | Fecha/hora del ticket (ISO 8601) |
channel | string | Canal de comunicacion: WHATSAPP, WEB, FACEBOOK, TELEGRAM, etc. |
Objeto event (datos de la reserva)
Campo | Tipo | Descripcion |
|---|---|---|
event.id | number | ID unico del evento en AsisteClick |
event.calendar_id | number | ID del calendario |
event.calendar_title | string | Nombre del calendario |
event.utc_from | string | Inicio de la cita en UTC (ISO 8601) |
event.utc_to | string | Fin de la cita en UTC (ISO 8601) |
event.location | string | Ubicacion de la cita |
event.public_key | string | Codigo de confirmacion visible al cliente |
Objeto customer (datos del cliente)
Campo | Tipo | Descripcion |
|---|---|---|
customer.name | string | Nombre completo del cliente |
customer.email | string/null | Email del cliente |
customer.phone | string | Telefono del cliente |
customer.country_code | string | Codigo de pais ISO (ej: "AR", "MX", "ES") |
Response esperado
Tu endpoint debe responder con status 200 OK para indicar que recibio correctamente el callback:
{
"status": "ok",
"message": "Reserva procesada correctamente",
"external_id": "TU-REF-12345"
}El contenido del response es informativo y no afecta el flujo de AsisteClick.
Ejemplo de implementacion del callback
// Ejemplo en Node.js con Express
app.post('/api/turnero/callback', async (req, res) => {
const { event_type, customer, event, custom_fields } = req.body;
// Verificar token de autenticacion
const token = req.headers['asisteclick-access-token'];
if (!verificarToken(token)) {
return res.status(401).json({ error: 'Token invalido' });
}
try {
switch (event_type) {
case 'EVENT.CREATED':
// Crear reserva en tu sistema
await crearReserva({
fecha: event.utc_from,
duracion: calcularMinutos(event.utc_from, event.utc_to),
cliente: customer.name,
telefono: customer.phone,
email: customer.email,
ubicacion: event.location,
codigo_asisteclick: event.public_key,
datos_adicionales: custom_fields
});
break;
case 'EVENT.UPDATED':
// Actualizar reserva existente
await actualizarReserva(event.public_key, {
nuevaFecha: event.utc_from
});
break;
case 'EVENT.CANCELED':
// Cancelar reserva
await cancelarReserva(event.public_key);
break;
}
res.json({ status: 'ok' });
} catch (error) {
console.error('Error procesando callback:', error);
res.status(500).json({ error: 'Error interno' });
}
});Ejemplo Completo: Clinica Medica
Escenario
Una clinica quiere que su sistema de gestion de pacientes (HIS) sea la fuente de verdad para los turnos, pero usar el chatbot de AsisteClick como interfaz de atencion.
Implementacion del Endpoint de Disponibilidad
<?php
// disponibilidad.php
header('Content-Type: application/json');
// Recibir datos de AsisteClick
$input = json_decode(file_get_contents('php://input'), true);
$calendar_id = $input['calendar_id'];
$slots = $input['slots'];
$start_date = $input['start_date'];
$end_date = $input['end_date'];
// Mapear calendar_id de AsisteClick a profesional en HIS
$profesionales = [
5 => 'DR_GARCIA',
6 => 'DRA_MARTINEZ',
7 => 'DR_LOPEZ'
];
$profesional_id = $profesionales[$calendar_id] ?? null;
if (!$profesional_id) {
http_response_code(400);
echo json_encode(['error' => 'Calendario no mapeado']);
exit;
}
// Consultar turnos ocupados en el HIS
$conn = new mysqli('localhost', 'user', 'pass', 'his_db');
$stmt = $conn->prepare("
SELECT fecha_turno
FROM turnos
WHERE profesional_id = ?
AND fecha_turno BETWEEN ? AND ?
AND estado IN ('confirmado', 'en_espera')
");
$stmt->bind_param("sss", $profesional_id, $start_date, $end_date);
$stmt->execute();
$result = $stmt->get_result();
// Crear set de horarios ocupados
$ocupados = [];
while ($row = $result->fetch_assoc()) {
$ocupados[] = date('c', strtotime($row['fecha_turno']));
}
// Filtrar slots disponibles
$disponibles = array_filter($slots, function($slot) use ($ocupados) {
return !in_array($slot['iso'], $ocupados);
});
// Responder
echo json_encode([
'slots' => array_values($disponibles),
'showBeforeButton' => false,
'showAfterButton' => count($disponibles) > 0
]);Implementacion del Callback
<?php
// callback.php
header('Content-Type: application/json');
// Verificar token
$token = $_SERVER['HTTP_ASISTECLICK_ACCESS_TOKEN'] ?? '';
if ($token !== 'TU_ACCESS_TOKEN_SECRETO') {
http_response_code(401);
echo json_encode(['error' => 'No autorizado']);
exit;
}
$input = json_decode(file_get_contents('php://input'), true);
$event_type = $input['event_type'];
$customer = $input['customer'];
$event = $input['event'];
$custom_fields = $input['custom_fields'] ?? [];
$conn = new mysqli('localhost', 'user', 'pass', 'his_db');
// Mapear calendario a profesional
$profesionales = [
5 => 'DR_GARCIA',
6 => 'DRA_MARTINEZ'
];
$profesional_id = $profesionales[$event['calendar_id']] ?? 'NO_ASIGNADO';
switch ($event_type) {
case 'EVENT.CREATED':
// Buscar o crear paciente
$stmt = $conn->prepare("SELECT paciente_id FROM pacientes WHERE telefono = ?");
$stmt->bind_param("s", $customer['phone']);
$stmt->execute();
$result = $stmt->get_result();
if ($result->num_rows == 0) {
// Crear paciente nuevo
$stmt = $conn->prepare("
INSERT INTO pacientes (nombre, telefono, email, pais)
VALUES (?, ?, ?, ?)
");
$stmt->bind_param("ssss",
$customer['name'],
$customer['phone'],
$customer['email'],
$customer['country_code']
);
$stmt->execute();
$paciente_id = $conn->insert_id;
} else {
$paciente_id = $result->fetch_assoc()['paciente_id'];
}
// Crear turno
$stmt = $conn->prepare("
INSERT INTO turnos
(paciente_id, profesional_id, fecha_turno, fecha_fin, ubicacion,
codigo_externo, estado, origen)
VALUES (?, ?, ?, ?, ?, ?, 'confirmado', 'asisteclick')
");
$stmt->bind_param("isssss",
$paciente_id,
$profesional_id,
$event['utc_from'],
$event['utc_to'],
$event['location'],
$event['public_key']
);
$stmt->execute();
// Guardar campos personalizados
foreach ($custom_fields as $field) {
$stmt = $conn->prepare("
INSERT INTO turno_extras (turno_id, campo, valor)
VALUES (?, ?, ?)
");
$turno_id = $conn->insert_id;
$stmt->bind_param("iss", $turno_id, $field['id'], $field['value']);
$stmt->execute();
}
break;
case 'EVENT.UPDATED':
$stmt = $conn->prepare("
UPDATE turnos
SET fecha_turno = ?, fecha_fin = ?
WHERE codigo_externo = ?
");
$stmt->bind_param("sss",
$event['utc_from'],
$event['utc_to'],
$event['public_key']
);
$stmt->execute();
break;
case 'EVENT.CANCELED':
$stmt = $conn->prepare("
UPDATE turnos
SET estado = 'cancelado'
WHERE codigo_externo = ?
");
$stmt->bind_param("s", $event['public_key']);
$stmt->execute();
break;
}
echo json_encode([
'status' => 'ok',
'turno_id' => $conn->insert_id ?? null
]);Limites y Consideraciones
Limite | Valor | Nota |
|---|---|---|
Timeout del request | 5 segundos | Si tu API no responde a tiempo, se usa fallback |
Reintentos automaticos | 2 | Solo para errores 5xx |
Tamaño maximo del body | 1 MB | Para requests y responses |
Cantidad maxima de slots | Sin limite | Depende de la configuracion del calendario |
Consideraciones de seguridad
- Valida siempre el token AsisteClick-Access-Token en los callbacks
- Usa HTTPS para todos los endpoints
- Implementa rate limiting para evitar abusos
- Registra logs de todas las peticiones para debugging
Consideraciones de rendimiento
- Tu API de disponibilidad debe responder en menos de 3 segundos idealmente
- Cachea las consultas frecuentes si es posible
- Usa indices en las columnas de fecha en tu base de datos
Troubleshooting
Sintoma | Causa Probable | Solucion |
|---|---|---|
No llegan peticiones a mi endpoint | URL incorrecta o inaccesible | Verifica que la URL sea publica y accesible desde internet |
Siempre usa el motor nativo | Tu endpoint devuelve error o timeout | Revisa logs de tu servidor, optimiza tiempo de respuesta |
Los slots no coinciden con mi sistema | Zona horaria incorrecta | Usa siempre el campo iso (UTC) para comparar |
Callbacks no llegan | Campo api_callback_url vacio | Verifica que guardaste la URL en la configuracion |
Error 401 en callbacks | Token invalido | Usa el token correcto de la cuenta |
Duplicados en mi sistema | No verificas public_key | Usa public_key como clave unica para evitar duplicados |
Validacion y Testing
Probar el endpoint de disponibilidad
Usa curl o Postman para simular una peticion de AsisteClick:
curl -X POST https://tu-dominio.com/api/turnero/disponibilidad \
-H "Content-Type: application/json" \
-H "User-Agent: AsisteClick-Booking/2.0" \
-d '{
"calendar_id": 5,
"timezone": "-3",
"preferred": null,
"count": 6,
"start_date": "2025-01-15",
"end_date": "2025-01-22",
"slots": [
{
"text": "Miercoles 15 de Enero - 10:00",
"isoLocal": "2025-01-15T10:00:00",
"iso": "2025-01-15T13:00:00Z",
"id": "slot_001"
}
]
}'Probar el callback
curl -X POST https://tu-dominio.com/api/turnero/callback \
-H "Content-Type: application/json" \
-H "AsisteClick-Access-Token: TU_TOKEN" \
-d '{
"event_type": "EVENT.CREATED",
"ticket_id": "#TEST123",
"customer": {
"name": "Usuario Prueba",
"phone": "+5491155550000",
"email": "[email protected]"
},
"event": {
"id": 1,
"calendar_id": 5,
"utc_from": "2025-01-15T13:00:00Z",
"utc_to": "2025-01-15T13:30:00Z",
"location": "Consultorio Test",
"public_key": "TESTKEY1"
}
}'Preguntas Frecuentes
Puedo usar autenticacion adicional en mis endpoints?
Si. Puedes agregar headers personalizados en la URL de callback usando el formato:
https://tu-dominio.com/callback::Authorization:Bearer tu-token;X-Custom:valorQue pasa si mi sistema esta caido?
AsisteClick tiene fallback automatico al motor nativo para disponibilidad. Para callbacks, los reintentos son limitados, por lo que es importante tener alta disponibilidad en tu endpoint de callback.
Puedo tener diferentes endpoints para diferentes calendarios?
Actualmente cada calendario tiene su propia configuracion de API, por lo que puedes apuntar cada uno a un endpoint diferente.
Como identifico de que calendario viene una reserva en el callback?
El objeto event incluye calendar_id y calendar_title que te permiten identificar el origen.
Los callbacks incluyen la conversacion completa?
Si. El array messages contiene toda la conversacion entre el cliente y el bot/agente.