Agendar una cita, paso a paso
El recorrido típico de un sitio de reservas. Los ejemplos usan curl; al final está el mismo flujo en JavaScript.
export API=https://api.odontosys.com/publica/v1
export CLAVE="osk_3f9k2m7q_…"
1. Elegí la agenda
curl "$API/agendas" -H "Authorization: Bearer $CLAVE"
{
"agendas": [
{
"id": "12",
"nombre": "Dr. López",
"color": "#3366CC",
"duracionTurnoMin": 30,
"jornada": {
"lunes": { "desde": "08:00", "hasta": "20:00", "descanso": { "desde": "12:00", "hasta": "13:00" } },
"martes": { "desde": "08:00", "hasta": "20:00", "descanso": null },
"miercoles": null,
"jueves": { "desde": "08:00", "hasta": "14:00", "descanso": null },
"viernes": { "desde": "08:00", "hasta": "14:00", "descanso": null },
"sabado": null,
"domingo": null
},
"ultimoCambio": "2026-09-21T13:59:02-03:00"
}
]
}
jornada es el horario habitual de cada día (null = no trabaja). Los días puntuales con otro horario —un feriado, una tarde libre— no están acá: los aplica disponibilidad. Para mostrar horarios, usá siempre disponibilidad.
ultimoCambio cambia cada vez que alguien toca algo en esa agenda. Si lo guardás, podés evitar volver a pedir las citas cuando no cambió.
2. Pedí los horarios libres
curl "$API/agendas/12/disponibilidad?desde=2026-10-05&hasta=2026-10-09&duracionMin=30" \
-H "Authorization: Bearer $CLAVE"
{
"agenda": "12",
"duracionMin": 30,
"dias": [
{ "fecha": "2026-10-05", "trabaja": true,
"huecos": [ { "desde": "08:00", "hasta": "09:30" }, { "desde": "10:00", "hasta": "12:00" } ] },
{ "fecha": "2026-10-07", "trabaja": false, "huecos": [] }
]
}
- Devuelve intervalos libres, no turnos: vos decidís en qué minutos ofrecer inicios. Con turnos de 30 minutos, el hueco de 08:00 a 09:30 da tres: 08:00, 08:30 y 09:00.
duracionMindescarta los huecos más cortos. Si no lo mandás, se usaduracionTurnoMinde la agenda.desdeno puede ser anterior a hoy y el rango va como mucho de 31 días. Sidesdees hoy, lo que ya pasó no aparece libre.- Ya tiene en cuenta la jornada del día, el descanso y las citas que ocupan (las canceladas liberan el horario).
3. Identificá al paciente (opcional)
Si tu sistema sabe el celular, el correo o el número de ficha, podés buscar si ya es paciente de la clínica (permiso pacientes:buscar):
curl "$API/pacientes/buscar?celular=099123456" -H "Authorization: Bearer $CLAVE"
{ "pacientes": [ { "id": "678", "nombre": "Juan Gómez" } ] }
- Un solo criterio por pedido, con coincidencia exacta. Del celular se comparan los últimos 8 dígitos, así que
+59899123456y099123456encuentran al mismo paciente. - Devuelve como mucho 5 pacientes activos, sólo con id y nombre.
- Si vuelven varios o ninguno, no adivines: agendá como alguien sin ficha (paso 4) y que la clínica lo vincule. Agendar a nombre del paciente equivocado le manda el recordatorio a otra persona.
4. Creá la cita
curl -X POST "$API/citas" \
-H "Authorization: Bearer $CLAVE" \
-H "Content-Type: application/json" \
-d '{
"idAgenda": "12",
"fecha": "2026-10-05",
"hora": "08:30",
"duracionMin": 30,
"paciente": { "id": "678" },
"nota": "Primera consulta (reservó por la web)",
"referenciaExterna": "reserva-8812"
}'
Responde 201 con la cita y la cabecera Location: /publica/v1/citas/17680900:
{
"id": "17680900", "idAgenda": "12", "fecha": "2026-10-05", "hora": "08:30", "duracionMin": 30,
"tipo": "paciente", "paciente": { "id": "678", "nombre": "Juan Gómez" }, "nombre": "Juan Gómez",
"nota": "Primera consulta (reservó por la web)", "estado": "enEspera", "confirmada": false,
"creadaPorApi": true, "referenciaExterna": "reserva-8812",
"creada": "2026-09-21T14:03:11-03:00", "modificada": "2026-09-21T14:03:11-03:00"
}
Paciente con ficha o sin ficha:
paciente |
Queda en la agenda | Recordatorios |
|---|---|---|
{ "id": "678" } |
A nombre del paciente, vinculada a su ficha | Sí, los que tenga configurados la clínica |
{ "nombre": "Ana Ruiz", "telefono": "099555444" } |
Con ese nombre; el teléfono va en la nota | No |
En una cita sin ficha, la nota que devuelve la API empieza con el teléfono: "Tel: 099555444 · Primera consulta".
Los topes: nota 200 caracteres, nombre 100, telefono 30, referenciaExterna 64. La duración va de 5 a 480 minutos y la cita tiene que terminar a más tardar a las 23:59.
Lo que puede salir mal, en el orden en que se verifica:
| Respuesta | Qué pasó | Qué hacer |
|---|---|---|
400 solicitud_invalida |
La forma del pedido. detalle.campo dice cuál |
Corregir el pedido |
404 no_encontrado |
La agenda no existe, está inactiva o la clave no la ve | Volver a pedir GET /agendas |
422 paciente_invalido |
Ese paciente no existe o está inactivo | Agendar como alguien sin ficha |
422 fecha_fuera_de_rango |
La fecha es anterior a hoy o pasa de hoy + 365 días | |
422 duracion_invalida |
Menos de 5 o más de 480 minutos, o termina después de las 23:59 | |
422 agenda_no_trabaja |
Ese día la agenda no atiende | Ofrecer otro día |
422 fuera_de_horario |
La cita no entra entera en la jornada, o pisa el descanso | Ofrecer otro horario |
409 horario_ocupado |
Alguien ocupó ese horario desde que pediste la disponibilidad | Volver a pedir disponibilidad y ofrecer otro |
El 409 horario_ocupado es normal: entre que el paciente mira los horarios y confirma, la clínica o otro paciente pueden tomar el mismo. Tu sitio tiene que manejarlo con un mensaje amable, no con un error.
5. Reintentar sin duplicar: referenciaExterna
Si el POST se corta (un timeout, la red), no sabés si la cita se creó. Mandá siempre una referenciaExterna —el id de la reserva en tu sistema— y reintentá con la misma:
- Si la cita ya se había creado y sigue existiendo, la API responde
200(no201) con esa misma cita y no crea otra. - Si nunca se creó, la crea:
201. - Si esa referencia ya se usó para una cita que después se eliminó, responde
409 referencia_ya_usada: la referencia no se puede reusar, mandá otra.
La referencia es única por clínica, entre todas sus claves.
6. Consultar y eliminar
Con citas:crear ves las citas que creó la integración:
curl "$API/citas?desde=2026-10-01&hasta=2026-10-31" -H "Authorization: Bearer $CLAVE"
curl "$API/citas/17680900" -H "Authorization: Bearer $CLAVE"
Para cancelar una reserva, eliminá la cita:
curl -X DELETE "$API/citas/17680900" -H "Authorization: Bearer $CLAVE"
Responde 204 sin cuerpo. Sólo se pueden eliminar citas que creó la API de la clínica (con cualquiera de sus claves), de hoy en adelante, y que la clínica todavía no haya marcado (asistencia o llegada a la sala de espera):
| Respuesta | Qué pasó |
|---|---|
403 cita_no_creada_por_api |
La cita la cargó la clínica: la API no la puede eliminar |
409 cita_no_eliminable |
Ya pasó, o la clínica ya marcó la asistencia o la llegada |
410 cita_eliminada |
Ya estaba eliminada |
410 cita_reprogramada |
La clínica la movió: ver abajo |
Eliminar es definitivo, igual que en Odontosys: la cita sale de la agenda y queda registrada en el Historial de actividad de la clínica como hecha por tu clave.
Si la clínica la mueve
Cuando la clínica cambia el día, la hora, la agenda o el paciente de una cita, Odontosys la elimina y la vuelve a crear con otro id. Para tu integración:
GEToDELETEsobre el id viejo responde410 cita_reprogramada: la cita sigue existiendo, con otro id.- La cita nueva ya no figura como creada por la API (
creadaPorApi: false) y sureferenciaExternano la acompaña. Con sólocitas:creardeja de verse.
Si tu sistema necesita seguirla, buscala con GET /citas en el rango de fechas (necesita citas:leer) y avisale al paciente que la clínica la cambió. En esta versión no hay avisos automáticos (webhooks) ni ids que sobrevivan al reagendamiento.
El mismo flujo en JavaScript
const API = "https://api.odontosys.com/publica/v1";
const CLAVE = process.env.ODONTOSYS_CLAVE; // nunca en el código ni en el navegador
async function api(metodo, ruta, cuerpo) {
const r = await fetch(API + ruta, {
method: metodo,
headers: { Authorization: `Bearer ${CLAVE}`, "Content-Type": "application/json" },
body: cuerpo ? JSON.stringify(cuerpo) : undefined,
});
if (r.status === 204) return null;
const json = await r.json();
if (!r.ok) {
const e = new Error(json.error.mensaje);
Object.assign(e, { status: r.status, codigo: json.error.codigo, idSolicitud: json.error.idSolicitud });
throw e;
}
return json;
}
// Horarios libres de la semana
const { dias } = await api("GET", "/agendas/12/disponibilidad?desde=2026-10-05&hasta=2026-10-09");
// Reservar, reintentando sin duplicar
try {
const cita = await api("POST", "/citas", {
idAgenda: "12", fecha: "2026-10-05", hora: "08:30", duracionMin: 30,
paciente: { nombre: "Ana Ruiz", telefono: "099555444" },
referenciaExterna: "reserva-8813",
});
console.log("Reservada:", cita.id);
} catch (e) {
if (e.codigo === "horario_ocupado") {
// mostrar otros horarios
} else {
throw e;
}
}
Para los 429 y 503, ver Límites y reintentos.