API: citas y viajes
Consultar la agenda de citas y los viajes con sus estados, filtros por cliente, empleado o conductor, sus webhooks y todos sus campos.
Revisado el 28 de octubre de 2026
La cita
{
"id": "d6e7f8a9-b0c1-4d2e-9f3a-4b5c6d7e8f9a",
"object": "appointment",
"status": "confirmed",
"service": "Corte de cabello",
"starts_at": "2026-10-02T15:00:00.000Z",
"duration_minutes": 45,
"price": "8.00",
"deposit_amount": "2.00",
"employee": { "id": "e7f8a9b0-c1d2-4e3f-8a4b-5c6d7e8f9a0b", "name": "Ana" },
"note": null,
"customer": {
"id": "c4d5e6f7-a8b9-4c0d-9e1f-2a3b4c5d6e7f",
"name": "María Pérez",
"phone": "584140000000"
},
"created_at": "2026-09-29T13:40:10.221Z",
"updated_at": "2026-09-29T13:52:02.876Z"
}| Campo | Tipo | Qué es |
|---|---|---|
id | uuid | Id de la cita. |
object | texto | Siempre appointment. |
status | texto | Ver estados. |
service | texto | El servicio, con el nombre que tenía al agendarse. |
starts_at | fecha | Cuándo empieza (UTC). |
duration_minutes | entero | Cuánto dura. |
price | importe | null | Precio del servicio, en la moneda base del negocio. |
deposit_amount | importe | null | La seña (anticipo) que hay que pagar para confirmarla, si el negocio la pide. |
employee | objeto | null | { id, name } de quien atiende, o null si no se asignó. |
note | texto | null | Nota de la cita. |
location | texto | local (en el negocio) o home (a domicilio). |
address | texto | null | A domicilio: la dirección. |
options | lista | Lo que eligió del servicio: { group, name, price, minutes? }. Vacía si nada. |
travel_fee | importe | null | A domicilio: el traslado cobrado. Ya va dentro de price. |
customer | objeto | Resumen del cliente: { id, name, phone }. |
created_at | fecha | Cuándo se agendó. |
updated_at | fecha | Último cambio. |
Estados de una cita
| status | En el panel | Qué significa |
|---|---|---|
requested | Solicitada | Pedida por el cliente, todavía sin confirmar (por ejemplo, esperando la seña). |
confirmed | Confirmada | Confirmada. |
completed | Completada | Se atendió. |
cancelled | Cancelada | Cancelada. |
no_show | No asistió | El cliente no vino. |
Una cita con seña se confirma sola cuando los pagos aprobados cubren la seña (puede pagarse en varias partes; ver aprobar un comprobante); si el pago se rechaza, queda en requested.
Listar, ver y agendar citas
Ordenadas por la hora de inicio, de la más tardía a la más temprana, paginado. Filtros opcionales:
| Parámetro | Qué filtra |
|---|---|
status | Uno de los estados de arriba. Otro valor: 400 invalid_status. |
from | Citas que empiezan en esa fecha o después (ISO 8601). Mal escrita: 400 invalid_date. |
to | Citas que empiezan en esa fecha o antes. |
customer_id | Las citas de un cliente. Si no es un uuid: 400 invalid_id. |
employee_id | Las de quien atiende (el employee.id de la cita). Si no es un uuid: 400 invalid_id. |
updated_after | Las que cambiaron en esa fecha o después. Ojo: con este filtro el orden pasa a ser del cambio más viejo al más nuevo, no por hora de inicio (ver sincronización incremental). |
# La agenda confirmada del 2 de octubre (hora de Venezuela)
curl "https://api.atiendefacil.com/api/v1/appointments?status=confirmed&from=2026-10-02T00:00:00-04:00&to=2026-10-02T23:59:59-04:00" \
-H "Authorization: Bearer af_live_TU_CLAVE_AQUI"Una cita. Una cita borrada responde 404.
Agenda una cita. Hace lo mismo que Nueva cita en el panel y la app: nace confirmed y se le avisa a quien la atiende. Responde 201 con la cita.
| Campo | Obligatorio | Qué es |
|---|---|---|
customer_id / customer_phone | Uno de los dos | El cliente. Con el teléfono, si no existe se crea; customer_name es opcional. |
service | Sí | El servicio. Si coincide con uno del catálogo, toma su precio y duración. |
starts_at | Sí | ISO 8601. Sin zona se lee en la hora del negocio. No puede ser en el pasado. |
duration_minutes | No | De 5 a 480. Por defecto, la del servicio o 30. |
employee_id | No | Quién atiende (su id en GET /team). Tiene que atender al público. |
note | No | Nota de la cita. |
location | No | local (por defecto) o home. A domicilio hace falta que el negocio lo tenga activado; se suma el traslado. |
address | No | A domicilio: la dirección. Sin ella, la que tenga el cliente. |
options | No | Opciones del servicio: [{ group?, name }]. Si el servicio tiene un grupo obligatorio, hay que mandarlo. |
curl -X POST https://api.atiendefacil.com/api/v1/appointments \
-H "Authorization: Bearer af_live_TU_CLAVE_AQUI" \
-H "Content-Type: application/json" \
-d '{
"customer_phone": "04140000000",
"customer_name": "María González",
"service": "Corte de cabello",
"starts_at": "2026-10-05T15:00:00-04:00",
"duration_minutes": 45
}'Errores: 400 invalid_body (también si el negocio está cerrado o esa persona no atiende a esa hora), 409 slot_unavailable (ya hay una cita o no queda cupo) y 404 not_found (cliente o empleado).
Horas libres
Los ratos libres de un rango, dentro del horario de atención: los mismos huecos que enseña la agenda del panel. Úsalo para ofrecer horas antes de agendar. Sin paginar.
| Parámetro | Obligatorio | Qué es |
|---|---|---|
from, to | Sí | El rango, en ISO 8601 (sin zona se lee en la hora del negocio). Hasta 31 días. |
employee_id | No | Los huecos de esa persona, contra su horario. Sin él, los del negocio contando las citas de todos. |
min_minutes | No | Solo los huecos de al menos esos minutos (0 a 480). |
curl "https://api.atiendefacil.com/api/v1/appointments/availability?from=2026-10-12T00:00:00&to=2026-10-13T00:00:00&min_minutes=30" \
-H "Authorization: Bearer af_live_TU_CLAVE_AQUI"{
"data": [
{ "object": "slot", "starts_at": "2026-10-12T13:00:00.000Z", "ends_at": "2026-10-12T15:30:00.000Z", "minutes": 150 },
{ "object": "slot", "starts_at": "2026-10-12T16:15:00.000Z", "ends_at": "2026-10-12T22:00:00.000Z", "minutes": 345 }
]
}- Solo salen huecos futuros y de 15 minutos o más. Un día cerrado no trae ninguno.
- Cada hueco es un tramo continuo: en uno de 10:00 a 12:00 cabe una cita de 30 minutos a las 10:00, a las 10:30, a las 11:00…
- Es una guía, no una reserva: quien decide es
POST /appointments, que puede responder409 slot_unavailablesi alguien tomó la hora entre medias. Si el negocio atiende varias citas a la vez (cupo mayor que 1) y no mandasemployee_id, puede caber una cita en una hora que aquí no sale libre. - Rango mal escrito, al revés o de más de 31 días:
400 invalid_date.
Mover, confirmar o cancelar una cita
Cambia una cita, como al editarla en el panel o la app. Manda solo lo que cambia. Responde la cita.
| Campo | Qué es |
|---|---|
status | confirmed (confirmarla), cancelled (cancelarla), completed (se atendió), no_show (no vino) o requested. |
starts_at | La hora nueva. ISO 8601; sin zona, la del negocio. No puede ser en el pasado. |
duration_minutes | De 5 a 480. |
employee_id | Quién atiende, o null para dejarla sin asignar. |
service | El nombre del servicio. No cambia el precio que ya tenía la cita. |
note | La nota, o null para borrarla. |
# Mover la cita al día siguiente a las 10:00 y dejarla confirmada
curl -X PATCH https://api.atiendefacil.com/api/v1/appointments/d6e7f8a9-b0c1-4d2e-8f3a-4b5c6d7e8f9a \
-H "Authorization: Bearer af_live_TU_CLAVE_AQUI" \
-H "Content-Type: application/json" \
-d '{ "starts_at": "2026-10-06T10:00:00-04:00", "status": "confirmed" }'- Al cambiar la hora, la duración o quién atiende se valida lo mismo que al agendar:
400 invalid_body(pasado, negocio cerrado o esa persona no atiende a esa hora) y409 slot_unavailable(solape o sin cupo). - Mover la hora reinicia los recordatorios: al cliente se le recordará la hora nueva.
- Este cambio no le escribe al cliente: si mueves o cancelas una cita, avísale tú con
POST /messages. - Salen
appointment.status_changedy/oappointment.rescheduled, según lo que cambie. - No hay borrado por la API: una cita que no va se cancela.
Webhooks de citas
| Evento | Cuándo sale |
|---|---|
appointment.created | Se agendó una cita. |
appointment.status_changed | Cambió el estado. Trae status y previous_status. |
appointment.rescheduled | Cambió la hora, la duración, el servicio o quién atiende. Trae previous_starts_at. Si en el mismo cambio también cambió el estado (por ejemplo, la cita movida vuelve a requested), salen los dos: appointment.status_changed y appointment.rescheduled. |
appointment.deleted | Se borró. El objeto solo trae el id. |
Ejemplos de cada uno en Webhooks.
El viaje
En modo transporte, cada vehículo pedido es un viaje: si el cliente pide tres motos de una vez, son tres viajes con el mismo group.id, cada uno con su conductor y su tarifa.
{
"id": "f8a9b0c1-d2e3-4f4a-9b5c-6d7e8f9a0b1c",
"object": "ride",
"status": "in_progress",
"vehicle_type": "mototaxi",
"vehicle_class": "Moto",
"pickup": { "address": "C.C. Sambil, entrada principal", "lat": 10.4893, "lng": -66.8532 },
"dropoff": { "address": "Plaza Altamira", "lat": 10.4961, "lng": -66.8487 },
"distance_km": "4.10",
"duration_minutes": 12,
"fare": "3.00",
"currency": "USD",
"driver": { "id": "a9b0c1d2-e3f4-4a5b-8c6d-7e8f9a0b1c2d", "name": "Pedro" },
"group": { "id": null, "index": 1, "size": 1 },
"payment": { "method": null, "reference": null, "amount": null, "paid_at": null },
"cancel_reason": null,
"customer": {
"id": "c4d5e6f7-a8b9-4c0d-9e1f-2a3b4c5d6e7f",
"name": "María Pérez",
"phone": "584140000000"
},
"accepted_at": "2026-09-29T19:02:44.310Z",
"started_at": "2026-09-29T19:09:12.005Z",
"completed_at": null,
"cancelled_at": null,
"created_at": "2026-09-29T19:02:01.771Z",
"updated_at": "2026-09-29T19:09:12.005Z"
}| Campo | Tipo | Qué es |
|---|---|---|
id | uuid | Id del viaje. |
object | texto | Siempre ride. |
status | texto | Ver estados. |
vehicle_type | texto | La familia de vehículo: mototaxi (moto) o taxi (carro). Es el valor interno; en pantalla el negocio ve el nombre de la clase. |
vehicle_class | texto | null | La clase que pidió el cliente, con el nombre que le puso el negocio («Moto», «Carro cómodo»). |
pickup | objeto | { address, lat, lng } del punto de recogida. Cualquiera puede ser null. |
dropoff | objeto | Lo mismo, del destino. |
distance_km | importe | null | Distancia del recorrido, en km. |
duration_minutes | entero | null | Duración estimada del recorrido. |
fare | importe | Tarifa del viaje. |
commission | importe | Lo que se queda la agencia de esa tarifa. |
driver_earnings | importe | Lo que recibe el conductor (tarifa menos comisión). |
currency | texto | Moneda de la tarifa. |
driver | objeto | null | { id, name } del conductor, o null mientras nadie lo tomó. |
tracking_url | texto | null | El enlace de seguimiento en vivo que recibe el pasajero. null si el negocio lo tiene apagado o el viaje ya terminó. |
group | objeto | id de la solicitud (null si fue un solo vehículo), index (cuál de ellos es) y size (cuántos se pidieron). |
payment | objeto | Cómo se pagó: method (pago_movil, transferencia, cash o null), reference, amount (importe) y paid_at. |
cancel_reason | texto | null | Por qué se canceló. |
customer | objeto | Resumen del cliente: { id, name, phone }. |
accepted_at | fecha | null | Cuándo lo aceptó el conductor. Igual started_at (empezó), completed_at y cancelled_at. |
created_at | fecha | Cuándo se pidió. |
updated_at | fecha | Último cambio. |
Estados de un viaje
| status | En el panel | Qué significa |
|---|---|---|
pending_payment | Esperando pago | Viaje con cobro por adelantado: existe y tiene tarifa, pero no se le ofrece a ningún conductor hasta que se confirme el pago. |
requested | Buscando conductor | Se está ofreciendo a los conductores en turno. |
accepted | Aceptado | Un conductor lo tomó. |
driver_arriving | Llegando | El conductor va hacia el punto de recogida. |
in_progress | En viaje | El pasajero (o el envío) va en camino al destino. |
completed | Completado | Terminó. Es final. |
cancelled | Cancelado | Se canceló. Es final; el motivo va en cancel_reason. |
Listar, ver y crear viajes
Del más nuevo al más viejo, paginado. Filtros opcionales (se combinan):
| Parámetro | Qué filtra |
|---|---|
status | Uno de los estados de arriba. Otro valor: 400 invalid_status. |
customer_id | Los viajes de un cliente. Si no es un uuid: 400 invalid_id. |
driver_id | Los de un conductor (el driver.id del viaje, o su id en GET /team). Si no es un uuid: 400 invalid_id. |
created_after, created_before | Viajes pedidos en esa fecha o después / antes (ISO 8601). Mal escrita: 400 invalid_date. |
updated_after | Los que cambiaron desde esa fecha, del más viejo al más nuevo (ver sincronización incremental). |
# Los viajes completados de un conductor en septiembre
curl "https://api.atiendefacil.com/api/v1/rides?driver_id=a9b0c1d2-e3f4-4a5b-8c6d-7e8f9a0b1c2d&status=completed&created_after=2026-09-01T00:00:00-04:00&created_before=2026-09-30T23:59:59-04:00" \
-H "Authorization: Bearer af_live_TU_CLAVE_AQUI"Un viaje.
Los viajes se piden por WhatsApp o se crean a mano (abajo), y los mueven los conductores desde la app (ver Modo transporte); el negocio puede asignarlos, cancelarlos y cobrarlos también por la API (más abajo). Para seguirlos en vivo, usa los webhooks ride.created y ride.status_changed. El pago de un viaje se aprueba o rechaza como cualquier otro (ver pagos).
Crear un viaje
Las clases de vehículo activas (id, name, kind mototaxi o taxi, description, capacity y su precio propio base_fee, per_km, min_fee, null cuando cobran la tarifa general). Su id es el vehicle_class_id de las dos llamadas de abajo.
Busca los dos puntos en el mapa y cotiza, sin crear nada. Úsalo para enseñar la ruta antes de mandar a un conductor.
curl -X POST https://api.atiendefacil.com/api/v1/rides/quote \
-H "Authorization: Bearer af_live_TU_CLAVE_AQUI" \
-H "Content-Type: application/json" \
-d '{
"pickup": { "query": "Farmacia San José, calle Comercio" },
"dropoff": { "query": "Terminal de pasajeros" }
}'{
"pickup": { "lat": 7.8891, "lng": -67.4735, "address": "Farmacia San José, Calle Comercio, San Fernando de Apure" },
"dropoff": { "lat": 7.9012, "lng": -67.4581, "address": "Terminal de Pasajeros, San Fernando de Apure" },
"distance_km": 3.4,
"duration_minutes": 9,
"fare": 2.5,
"vehicles": 1,
"total": 2.5,
"route_url": "https://www.google.com/maps/dir/?api=1&origin=7.8891,-67.4735&destination=7.9012,-67.4581",
"prepaid": false
}Crea el viaje. Hace lo mismo que Nuevo viaje en el panel y la app: busca conductor (o espera el pago, si el negocio cobra por adelantado) y le manda la confirmación al pasajero. Responde 201 con { data: [viaje…], total, prepaid }: un viaje por vehículo.
| Campo | Obligatorio | Qué es |
|---|---|---|
customer_id / customer_phone | Uno de los dos (solo en /rides) | El pasajero. Con el teléfono, si no existe se crea; customer_name es opcional. |
pickup, dropoff | Sí | Cada uno es { query } (texto a buscar en el mapa) o { lat, lng, address? } (el punto ya resuelto). |
vehicle_class_id | No | La clase de vehículo. Sin ella, la única que haya o la tarifa general. |
vehicles | No | De 1 a 6. Por defecto 1. |
distance_km, duration_minutes | No (solo en /rides) | Los que devolvió /rides/quote: así la ruta no se mide otra vez. |
note | No (solo en /rides) | Recado para el conductor. |
# Con los puntos y los km que devolvió /rides/quote: no se buscan ni se miden otra vez
curl -X POST https://api.atiendefacil.com/api/v1/rides \
-H "Authorization: Bearer af_live_TU_CLAVE_AQUI" \
-H "Content-Type: application/json" \
-d '{
"customer_phone": "04140000000",
"pickup": { "lat": 7.8891, "lng": -67.4735, "address": "Farmacia San José, Calle Comercio" },
"dropoff": { "lat": 7.9012, "lng": -67.4581, "address": "Terminal de Pasajeros" },
"distance_km": 3.4,
"duration_minutes": 9,
"note": "Lleva dos maletas"
}'La tarifa de la cotización no incluye el acercamiento del conductor: se suma al asignarlo. Cada búsqueda en el mapa y cada ruta descuentan saldo del negocio, salvo que la misma se haya calculado en los últimos 30 días.
| Error | Cuándo |
|---|---|
| 400 invalid_body | Falta un punto, el cliente o algo tiene mal formato. |
| 409 pickup_not_found / dropoff_not_found | Ese texto no salió en el mapa. |
| 409 pickup_out_of_area / dropoff_out_of_area | El punto queda fuera de la zona del negocio. |
| 409 out_of_zone | El recorrido no cae en ninguna zona de tarifa. |
| 409 same_point | La recogida y el destino son el mismo punto. |
| 409 transport_disabled | El negocio no tiene el modo transporte. |
| 503 maps_unavailable | La búsqueda en el mapa no respondió; reintenta. |
Viajes sin conductor
ride.needs_driver sale cuando ya no queda ningún conductor a quien ofrecerle el viaje: al pedirlo no había nadie cerca y libre (data.reason = "no_drivers_available"), o se le ofreció a todos los posibles y lo rechazaron, lo dejaron vencer o lo cancelaron ("no_drivers_left"). Un rechazo suelto no lo dispara: mientras quede alguien, el sistema se lo ofrece solo al siguiente.
A partir de ahí el viaje sigue en requested con driver: null y el cliente esperando: hay que asignarlo a mano, desde el panel, la app o con POST /rides/{id}/assign (abajo). Un viaje prepagado que espera el pago no lo dispara: todavía no se buscó a nadie.
Asignar, cancelar y cobrar un viaje
Lo que el negocio hace en la ficha del viaje, por las mismas funciones y con los mismos avisos al pasajero y al conductor. Todas responden el viaje y no llevan más cuerpo que el indicado.
Le pone el viaje a un conductor: cuerpo { "driver_id": "…" } (su id en GET /fleet o GET /team). Es una asignación, no una oferta: el viaje queda accepted sin preguntarle, aunque esté fuera de turno o sin ubicación, y al pasajero le llegan los datos del conductor. Sirve también para pasárselo a otro mientras no haya empezado.
Cancela ese vehículo (no la solicitud entera: si pidió tres, cancela cada uno). Cuerpo opcional { "reason": "…" }, hasta 200 caracteres: se le manda al pasajero y queda en cancel_reason. Avisa al conductor si tenía uno.
Da por bueno el pago por adelantado de un viaje en pending_payment y lo despacha: empieza a buscar conductor. Confirma la solicitud entera (todos los vehículos del mismo group.id). Si hay un comprobante por revisar, también vale aprobarlo con POST /payments/{id}/approve.
Da por cobrado un viaje que quedó pendiente (el efectivo que recibió el conductor y no marcó). Cuerpo opcional { "payment_method": "cash" | "pago_movil" | "transferencia" }.
Vuelve a medir la ruta y a cotizarla con las tarifas de hoy, para toda la solicitud. Si la tarifa cambia, se le avisa al pasajero con la cifra nueva. Mide en el mapa, así que descuenta saldo.
| Error | Cuándo |
|---|---|
| 404 not_found | El viaje no existe. |
| 404 driver_not_found | assign: ese conductor no existe o no está activo. |
| 409 driver_busy | assign: ese conductor ya lleva otro viaje. |
| 409 already_assigned | assign: el viaje ya es de ese conductor y ya lo aceptó. |
| 409 ride_closed | assign: el viaje espera el pago, ya empezó, terminó o se canceló. |
| 409 not_cancellable | cancel: ya terminó o ya estaba cancelado. |
| 409 not_awaiting_payment | confirm-payment: no espera un pago (ya confirmado, a crédito o cancelado). |
| 409 not_payable | paid: ya está cobrado, o cancelado. |
| 409 too_late / no_route / out_of_zone | recalculate: ya cobrado o cerrado, sin puntos en el mapa, o fuera de las zonas de tarifa. |
Preguntas frecuentes
¿Cómo saco las citas de mañana en orden?
Filtra con from y to y ordénalas tú por starts_at: la API las devuelve de la más tardía a la más temprana.
¿Puedo agendar o cancelar citas por la API?
Sí: se agenda con POST /appointments y se mueve, confirma o cancela con PATCH /appointments/{id}. Para saber qué horas ofrecer, GET /appointments/availability.
