Conceptos
La clave es la clínica
Cada clave pertenece a una clínica y todo lo que hace queda dentro de ella. Por eso:
- Ningún pedido lleva un id de clínica. Si mandás
idClinica, la API responde400. - Un id que no es de la clínica —una agenda, un paciente, una cita de otra— responde
404 no_encontrado, igual que si no existiera. La API nunca confirma que algo exista afuera de tu clínica. - Si integrás varias clínicas, cada una te da su clave. Guardá cada clave junto a la clínica a la que pertenece.
Permisos
La clínica elige los permisos al crear la clave y no se pueden cambiar después: si hace falta otro, crea una clave nueva. La regla es "lo propio contra todo": el permiso básico sólo ve lo que la integración creó; ver el resto de la agenda es un permiso aparte.
| Permiso | Habilita |
|---|---|
agendas:leer |
GET /agendas y GET /agendas/{id}/disponibilidad |
pacientes:buscar |
GET /pacientes/buscar (devuelve sólo id y nombre) |
citas:crear |
POST /citas, DELETE /citas/{id}, y GET /citas y GET /citas/{id} limitados a las citas que creó la API de la clínica |
citas:leer |
GET /citas y GET /citas/{id} sobre todas las citas de las agendas permitidas, con nombres de pacientes y notas |
GET /ping no pide ningún permiso. Un pedido sin el permiso que corresponde responde 403 permiso_insuficiente, y detalle.permisosRequeridos dice cuál falta.
Pedile a la clínica sólo lo que la integración necesita. Un sitio de reservas típico anda con agendas:leer y citas:crear; pacientes:buscar suma si querés reconocer a los pacientes que ya tienen ficha.
Agendas permitidas
La clínica puede limitar la clave a algunas agendas. Para la integración, las demás no existen: no aparecen en GET /agendas y cualquier pedido sobre ellas responde 404. GET /ping muestra la lista en clave.agendasPermitidas (null = todas).
Una agenda que la clínica desactiva desaparece también: deja de listarse y sus citas dejan de verse por la API.
Fechas y horas: siempre en la hora de la clínica
La API trabaja en la zona horaria de la clínica, la que figura en GET /ping (clinica.zonaHoraria, por ejemplo America/Montevideo). "Hoy" y "ahora" también se calculan ahí.
| Qué | Formato | Ejemplo |
|---|---|---|
| Fecha | AAAA-MM-DD |
2026-10-05 |
| Hora | HH:mm, de 00:00 a 23:59 |
09:30 |
| Instante | ISO-8601 con el desfasaje de la clínica | 2026-09-21T14:03:11-03:00 |
| Duración | minutos, entero | 30 |
Una fecha y una hora de una cita no llevan zona: "fecha": "2026-10-05", "hora": "09:30" son las 9:30 en el consultorio. No las conviertas a UTC.
Ids
Los ids viajan como texto ("12"), aunque hoy sean números: así se pueden cambiar más adelante sin romper la v1. En el cuerpo de POST /citas también se acepta un número (12), pero las respuestas los devuelven siempre como texto. Guardalos como texto.
⚠️ El id de una cita cambia si la clínica la reagenda. Al mover una cita de día, de hora, de agenda o de paciente, Odontosys la elimina y la vuelve a crear con otro id. Si pedís el id viejo, la API responde 410 cita_reprogramada. Ver Agendar una cita → Si la clínica la mueve.
Tipos de cita
Cada cita tiene un tipo:
paciente: de un paciente con ficha. Traepaciente: { id, nombre }y recibe los recordatorios que la clínica tenga configurados (WhatsApp, SMS o correo).noRegistrado: de alguien sin ficha. Trae el nombre ennombrey no recibe recordatorios.nota: no es una cita sino un bloqueo de la agenda (vacaciones, una reunión). Ocupa el horario.
Estado y confirmación
estado es la asistencia que marca la clínica: enEspera, atendida, llegoTarde, seAtendioTarde, falto, canceloATiempo, canceloTarde o canceloLaClinica. Las cuatro últimas liberan el horario: en disponibilidad vuelve a aparecer libre.
confirmada dice si el paciente confirmó que viene (por el recordatorio o porque lo marcó la clínica). Una cancelación del paciente por WhatsApp no la marca como confirmada, pero tampoco cambia el estado hasta que la clínica lo marque: mientras tanto la cita sigue ocupando el horario.
Paginación
GET /citas pagina con pagina (desde 1) y porPagina (por defecto 100, máximo 200). La respuesta trae paginacion: { pagina, porPagina, total }: seguí pidiendo mientras pagina × porPagina < total.
Formato de los pedidos
- JSON en UTF-8, en los dos sentidos. El cuerpo de un pedido llega hasta 64 KB.
- Un campo que no es del contrato responde
400: un error de tipeo nunca pasa en silencio. - Los textos no admiten emojis ni otros caracteres fuera del plano básico de Unicode: responden
400y el mensaje lo dice. - Toda respuesta trae la cabecera
X-Solicitud-Id. Guardala en tus registros: es lo que pide soporte para encontrar un pedido.