Agenda de turnos
Endpoint y callback para turnero integrado con sistema externo
48 min
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/disponibilidad este 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/callback este 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 importante solo debes devolver los slots que esten realmente disponibles en tu sistema asisteclick mostrara exactamente lo que devuelvas 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" "juan\@ejemplo com", "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" "test\@ejemplo com" }, "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\ valor que 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
