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 true 220,220,221 unhandled content type unhandled content type unhandled content type unhandled content type unhandled content type unhandled content type unhandled content type unhandled content type unhandled content type unhandled content type unhandled content type unhandled content type unhandled content type unhandled content type unhandled content type unhandled content type unhandled content type unhandled content type unhandled content type unhandled content type unhandled content type unhandled content type unhandled content type unhandled content type descripcion de cada slot true 220,220,221 unhandled content type unhandled content type unhandled content type unhandled content type unhandled content type unhandled content type unhandled content type unhandled content type unhandled content type unhandled content type unhandled content type unhandled content type unhandled content type unhandled content type unhandled content type 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 true 165,165,165,166 unhandled content type unhandled content type unhandled content type unhandled content type unhandled content type unhandled content type unhandled content type unhandled content type unhandled content type unhandled content type unhandled content type unhandled content type unhandled content type unhandled content type unhandled content type unhandled content type 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 true 330,331 unhandled content type unhandled content type unhandled content type unhandled content type unhandled content type unhandled content type unhandled content type unhandled content type unhandled content type unhandled content type 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 true 330,331 unhandled content type unhandled content type unhandled content type unhandled content type unhandled content type unhandled content type unhandled content type unhandled content type request que recibiras metodo post content type application/json headers adicionales true 330,331 unhandled content type unhandled content type unhandled content type unhandled content type unhandled content type unhandled content type 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 true 220,220,221 unhandled content type unhandled content type unhandled content type unhandled content type unhandled content type unhandled content type unhandled content type unhandled content type unhandled content type unhandled content type unhandled content type unhandled content type unhandled content type unhandled content type unhandled content type unhandled content type unhandled content type unhandled content type objeto event (datos de la reserva) true 220,220,221 unhandled content type unhandled content type unhandled content type unhandled content type unhandled content type unhandled content type unhandled content type unhandled content type unhandled content type unhandled content type unhandled content type unhandled content type unhandled content type unhandled content type unhandled content type unhandled content type unhandled content type unhandled content type unhandled content type unhandled content type unhandled content type unhandled content type unhandled content type unhandled content type objeto customer (datos del cliente) true 220,220,221 unhandled content type unhandled content type unhandled content type unhandled content type unhandled content type unhandled content type unhandled content type unhandled content type unhandled content type unhandled content type unhandled content type unhandled content type unhandled content type unhandled content type unhandled content type 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 true 220,220,221 unhandled content type unhandled content type unhandled content type unhandled content type unhandled content type unhandled content type unhandled content type unhandled content type unhandled content type unhandled content type unhandled content type unhandled content type unhandled content type unhandled content type unhandled content type 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 true 220,220,221 unhandled content type unhandled content type unhandled content type unhandled content type unhandled content type unhandled content type unhandled content type unhandled content type unhandled content type unhandled content type unhandled content type unhandled content type unhandled content type unhandled content type unhandled content type unhandled content type unhandled content type unhandled content type unhandled content type unhandled content type unhandled content type 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
