◈ GESTIÓN

Documentación API

← Volver

Documentación de la API

Cada negocio tiene su propia API REST bajo el prefijo /api/n/<slug>/. Todos los endpoints son compatibles con integraciones externas como agentes de IA (WhatsApp) o la web del negocio.

Autenticación

Todos los endpoints requieren autenticación. Se aceptan dos métodos:

Opción 1 — X-API-Key (integraciones externas)

Cada negocio tiene dos API keys, ambas en el panel de gestión (botón API):

X-API-Key: a3f9b2c1d4e5f6a7b8c9d0e1f2a3b4c5
⚠️ Importante: La API key de un negocio solo da acceso a los endpoints de ESE negocio (una key de otro negocio devuelve 401). La key web, además, no puede leer clientes ni citas ni anular/modificar reservas: es la única que conviene exponer a un navegador.

Opción 2 — JWT Bearer (dashboard web)

El dashboard del negocio usa JWT. Se obtiene al hacer login.

Authorization: Bearer <token>
Obtener JWT: POST /api/auth/login/ con {"username": "email", "password": "..."}

Respuesta sin autenticación

{ "ok": false, "error": "Autenticación requerida. Incluye X-API-Key o un JWT válido." }

El slug del negocio

Sustituye <slug> por el identificador del negocio. Lo encuentras en el listado principal junto a la URL del dashboard.

# Ejemplo para PIT-LANE: /api/n/pit-lane/disponibilidad/ # Ejemplo para Autolavado García: /api/n/autolavado-garcia/disponibilidad/

Listar servicios

GET /api/n/<slug>/servicios/ API Key / JWT Lista los servicios activos del negocio
Respuesta
{ "ok": true, "servicios": [ { "id": 1, "nombre": "OPTIMUM — Txiki", "descripcion": null, "precio": "99.00", "duracion_minutos": 70, "hora_max_reserva": "15:15", "es_simultaneo": false, "color_hex": "#3b82f6", "orden": 0, "caracteristica": "¿Los asientos son de tela o de piel?", "opciones": [ { "nombre": "Tela", "hora_max_reserva": "11:15", "horarios_especificos": "", "tiempo_estimado": "Todo el día — se recoge por la tarde", "orden": 0 }, { "nombre": "Piel", "hora_max_reserva": null, "horarios_especificos": "", "tiempo_estimado": "", "orden": 1 } ] } ] }
hora_max_reserva: hora límite de inicio del servicio. Si es null se calcula automáticamente como hora_cierre − duración. Úsala para mostrar al cliente cuándo es la última hora disponible.
caracteristica / opciones: si el servicio define una pregunta (caracteristica) con opciones, el agente debe hacérsela al cliente y enviar la elegida como opcion en /disponibilidad/, /reservas/crear/ y /reservas/modificar/. Cada opción puede endurecer la ventana horaria (hora_max_reserva, horarios_especificos) y personalizar el tiempo_estimado; campo vacío/null = hereda del servicio. Sin opciones, el servicio se comporta como siempre.

Slots disponibles

POST /api/n/<slug>/disponibilidad/ API Key / JWT Horas libres para una fecha y servicio
⚠️ El servicio es necesario para calcular la duración y la hora límite correctamente. Sin él se devuelve ok: true con la lista de servicios disponibles para que el agente los muestre al cliente.
CampoTipoRequeridoDescripción
fechastringFormato YYYY-MM-DD
serviciostringRecomendadoNombre del servicio. Ej: "OPTIMUM" o "OPTIMUM — Txiki". Acepta nombre exacto o prefijo.
servicio_idintegerAlternativaID del servicio (alternativa al nombre)
opcionstringCondicionalObligatoria si el servicio tiene caracteristica con opciones. Ej: "Tela" (no distingue mayúsculas ni espacios). Sin ella (o con un valor no listado) se devuelve ok: true con la pregunta y las opciones.
excluirstring (uuid)OpcionalID de una reserva a ignorar en la ocupación. Útil al modificar: la propia reserva no bloquea su hueco y su hora aparece como libre. Un valor que no sea UUID se ignora.
Body — con nombre del servicio (recomendado para agentes)
{ "fecha": "2026-06-03", "servicio": "OPTIMUM — Txiki" }
Respuesta — slots disponibles
{ "ok": true, "disponibles": ["08:15", "09:15", "10:15", "11:15"], "fecha": "2026-06-03", "dia_semana": "miércoles", "servicio": { "id": 1, "nombre": "OPTIMUM — Txiki", "duracion_minutos": 70, "hora_max_reserva": "15:15" } }
Eco de la fecha: toda respuesta con ok: true incluye fecha (la consultada, YYYY-MM-DD) y dia_semana ("lunes"…"domingo"). El agente debe usarlos al comunicar los huecos al cliente ("el lunes 10 tenemos…") en lugar de calcular el día él mismo — evita confirmar un día equivocado.
Respuesta — sin servicio (el agente debe preguntar cuál)
{ "ok": true, "disponibles": [], "mensaje": "Para consultar la disponibilidad necesito saber el servicio que deseas. Los servicios disponibles son: OPTIMUM — Txiki, VIP — Pequeño, LUXURY — Grande.", "fecha": "2026-06-03", "dia_semana": "miércoles" }
Respuesta — el servicio exige una opción (el agente debe preguntar)
{ "ok": true, "disponibles": [], "mensaje": "Para darte horas de 'Limpieza tapicería' necesito saber: ¿Los asientos son de tela o de piel? Opciones: Tela, Piel.", "caracteristica": { "pregunta": "¿Los asientos son de tela o de piel?", "opciones": ["Tela", "Piel"] } }
Con opción válida, el bloque servicio de la respuesta añade opcion (nombre canónico aplicado), caracteristica (la pregunta) y tiempo_estimado (el efectivo — útil para comunicar la recogida: "Todo el día — se recoge por la tarde").
Ojo con las tres formas del mismo concepto: en /servicios/, opciones es una lista de objetos (con overrides); en esta respuesta de "falta opción", caracteristica (nivel raíz) es un objeto {pregunta, opciones: [nombres]}; y con opción válida, servicio.caracteristica es un string (la pregunta). El nivel de anidación desambigua.
Respuesta — sin huecos por hora límite
{ "ok": true, "disponibles": [], "servicio": { "nombre": "VIP — Pequeño", "hora_max_reserva": "09:15", ... }, "mensaje": "No hay huecos disponibles para 'VIP — Pequeño'. Este servicio solo puede reservarse hasta las 09:15." }
Respuesta — día bloqueado desde el panel (el agente transmite el mensaje)
{ "ok": true, "disponibles": [], "mensaje": "El 2026-06-03 tenemos la agenda cerrada: no se aceptan reservas ese día.", "fecha": "2026-06-03", "dia_semana": "miércoles" }
Respuesta — festivo del negocio (el agente transmite el mensaje)
{ "ok": true, "disponibles": [], "mensaje": "El 2026-01-20 es festivo (San Sebastián): la agenda está cerrada y no se aceptan reservas ese día.", "fecha": "2026-01-20", "dia_semana": "martes" }
Nota: Los slots respetan la duración del servicio, la pausa al mediodía, los huecos ocupados, la hora máxima de inicio configurada por servicio, los días bloqueados desde el panel (check "Bloquear día" en la agenda) y los festivos del negocio (calendario configurado por la plataforma). En ambos casos las reservas existentes se mantienen, pero no entran nuevas; el mensaje ya viene redactado para transmitirlo al cliente.

Crear reserva

POST /api/n/<slug>/reservas/crear/ API Key / JWT Crea una nueva reserva
CampoTipoRequeridoDescripción
nombrestringNombre del cliente
telefonostringTeléfono del cliente
fechastringYYYY-MM-DD
horastringHH:MM (de los slots disponibles)
serviciostringNombre del servicio
preciostringEj: "99 €"
modelostringModelo del vehículo
tiempo_estimadostringEj: "1h 10min". Si se omite y la opción define uno propio, se usa el de la opción.
apellidosstringNo
emailstringNo
variantestringNoEj: "Txiki", "Grande"
opcionstringCondicionalObligatoria si el servicio tiene caracteristica. Sin ella (o con un valor no listado) → 400 con la pregunta tal cual en error (ej: "¿Los asientos son de tela o de piel? Opciones: Tela, Piel."). Se guarda en la reserva como caracteristica (nombre canónico del catálogo).
colorstringNoColor del vehículo
observacionesstringNo
canalstringNoCanal por el que entra la reserva: "whatsapp", "chat_web", "web" o "agente". Si se omite, el sistema lo deduce solo: JWT de miembro → panel; petición de navegador (con Origin) → web; API key server-to-server → agente. Un agente con varios canales (WhatsApp y chat web) debería mandarlo para distinguirlos.
Body
{ "nombre": "Ana", "apellidos": "García", "telefono": "600111222", "email": "ana@email.com", "fecha": "2026-06-03", "hora": "09:15", "servicio": "OPTIMUM — Txiki", "variante": "Txiki", "precio": "99 €", "modelo": "Audi A1 2023", "color": "Blanco", "tiempo_estimado": "1h 10min", "observaciones": "Notas opcionales" }
Respuesta 201
{ "ok": true, "ref": "PL-AB3X7K", "id": "550e8400-e29b-41d4-a716-446655440000" }
Respuesta — slot ocupado
{ "ok": false, "error": "Horario ocupado. Capacidad máxima alcanzada hasta las 11:15" }
Respuesta — hora límite superada
{ "ok": false, "reservada": false, "error": "'VIP — Pequeño' no puede reservarse más tarde de las 09:15. Con 480 min de duración no daría tiempo antes del cierre." }
Respuesta — día bloqueado desde el panel
{ "ok": false, "reservada": false, "error": "El 2026-06-03 la agenda está cerrada: no se aceptan nuevas reservas." }
Respuesta — festivo del negocio
{ "ok": false, "reservada": false, "error": "El 2026-01-20 es festivo (San Sebastián): la agenda está cerrada y no se aceptan nuevas reservas." }

Buscar reserva

POST /api/n/<slug>/reservas/buscar/ API Key / JWT Busca por nombre + fecha + hora
Body
{ "nombre": "Ana García", "fecha": "2026-06-03", "hora": "09:15" }
Respuesta
{ "ok": true, "encontrada": true, "reserva": { "id": "...", "ref": "PL-AB3X7K", "estado": "activa", ... } }

Anular reserva

POST /api/n/<slug>/reservas/anular/ API Key / JWT Cancela una reserva por ID
Body
{ "id": "550e8400-e29b-41d4-a716-446655440000" }
Respuesta
{ "ok": true, "mensaje": "Reserva PL-AB3X7K cancelada correctamente" }

Modificar reserva

POST /api/n/<slug>/reservas/modificar/ API Key / JWT Modifica campos de una reserva existente
Nota: Solo envía los campos que quieras cambiar, junto con el id. El sistema valida solapamientos excluyendo la propia reserva.
opcion: campo opcional que cambia la opción de la característica del servicio (se guarda en la reserva como caracteristica) y dispara la revalidación de la ventana horaria — ver nota al final de esta sección.
Body
{ "id": "550e8400-e29b-41d4-a716-446655440000", "fecha": "2026-06-04", "hora": "10:15" }
Respuesta
{ "ok": true, "reserva": { "id": "...", "fecha": "2026-06-04", "hora": "10:15", ... } }
Revalidación de agenda: si la modificación toca fecha, hora, servicio, variante u opcion, el backend revalida la ventana horaria efectiva (con la opción ya guardada en la reserva, o la nueva si se envía) y responde ok: false con el motivo si la hora ya no es válida. Editar solo datos de contacto nunca revalida. Si cambias a un servicio distinto con caracteristica, envía siempre opcion: es obligatoria cuando la reserva no tenía ninguna guardada; si tenía una (de otro servicio) se reutiliza contra el nuevo catálogo, pero puede no matchear ninguna de sus opciones. Al cambiar a un servicio sin caracteristica, el snapshot se limpia.
Días bloqueados y festivos: mover una reserva a un día bloqueado desde el panel o a un festivo del negocio responde ok: false con el motivo en error (el del festivo indica que lo es, con su descripción si la tiene). Editar una reserva que ya está en ese día (hora, teléfono…) sí se permite.

Citas por teléfono

POST /api/n/<slug>/citas/ API Key / JWT Todas las reservas activas de un teléfono
Body
{ "telefono": "600111222" }
Respuesta
{ "ok": true, "reservas": [ { "id": "...", "ref": "PL-AB3X7K", "fecha": "2026-06-03", "hora": "09:15", ... } ] }

Recordatorio de citas del día

POST /api/n/<slug>/reservas/recordatorio_citas_dia/ API Key / JWT Citas de una fecha para recordar (cliente, vehículo, servicio; sin precio)
Para qué sirve: las citas de un día para enviar el recordatorio ("mañana tienes cita"). Devuelve las reservas activas de esa fecha ordenadas por hora, con los datos del cliente (nombre, apellidos, telefono), del vehículo (modelo, color, matricula) y del servicio (servicio, variante), más id/ref y recordada. No incluye el precio; las canceladas quedan fuera.
solo_pendientes: envía "solo_pendientes": true para recibir solo las citas que aún no has recordado (las ya marcadas con marcar-recordada se excluyen). Así, al volver a pedir la lista, no se repiten los recordatorios. Sin el flag salen todas.
No es el aviso de "servicio terminado": esto recuerda una cita futura. Avisar de que el coche/comida ya está listo es otro endpoint (aviso-terminado).
Body
{ "fecha": "2026-06-03", "solo_pendientes": true }
Respuesta
{ "ok": true, "fecha": "2026-06-03", "reservas": [ { "id": "...", "ref": "PL-AB3X7K", "hora": "09:15", "estado": "activa", "servicio": "OPTIMUM", "variante": "Mediano", "nombre": "Ana", "apellidos": "García", "telefono": "600111222", "modelo": "Golf", "color": "Gris", "matricula": "1234ABC", "recordada": false } ] }
POST /api/n/<slug>/reservas/marcar-recordada/ API Key / JWT Marca una cita como ya recordada
Para qué sirve: llámalo tras enviar el recordatorio de una cita. A partir de ahí deja de aparecer en recordatorio_citas_dia con solo_pendientes: true. Es idempotente: volver a marcarla solo actualiza la fecha. El id del body es el de la reserva (campo id de la lista).
Body
{ "id": "e3f1c2a4-..." }
Respuesta
{ "ok": true, "cita_recordada_en": "2026-06-02T18:30:00+00:00" }

Consulta de cliente

POST /api/n/<slug>/clientes/consulta/ API Key / JWT Quién es el cliente y qué se hace en qué coches
Para qué sirve: contexto del cliente antes de atenderle. Devuelve su ficha (con las notas del negocio), sus coches con nº de limpiezas y última visita, el historial de visitas pasadas (máx. 10, la más reciente primero) y sus próximas citas con id y ref (útiles para modificar/anular sin pasar por buscar).
Teléfono flexible: se ignoran espacios y signos, y se prueban las variantes de prefijo (+34…, 34… y sin prefijo), así que el número puede enviarse tal cual llega de WhatsApp.
Body
{ "telefono": "+34600111222" }
Respuesta
{ "ok": true, "encontrado": true, "cliente": { "nombre": "Ana", "apellidos": "García", "telefono": "600111222", "email": "ana@example.com", "notas": "Cliente VIP, prefiere mañanas", "servicio_habitual": "OPTIMUM" }, "total_visitas": 7, "coches": [ { "modelo": "Golf", "color": "Gris", "matricula": "1234ABC", "limpiezas": 5, "ultima_visita": "2026-05-12", "ultimo_servicio": "OPTIMUM" } ], "historial": [ { "fecha": "2026-05-12", "hora": "10:15", "servicio": "OPTIMUM", "variante": "Mediano", "caracteristica": "Tela", "modelo": "Golf", "color": "Gris", "precio": "109 €", "observaciones": "" } ], "proximas_citas": [ { "id": "550e8400-...", "ref": "PL-AB3X7K", "fecha": "2026-08-01", "hora": "09:15", "servicio": "VIP", ... } ] }
Preferencias del panel: servicio_habitual es el servicio preferido que el negocio fija a mano en la ficha (cadena vacía si no hay). Los coches que el negocio registra a mano en la ficha también salen en coches: fusionados con los del historial si el modelo coincide, o como entrada nueva con limpiezas: 0 y ultima_visita: null si el cliente aún no ha venido. Así el agente conoce el coche y el servicio de un cliente aunque todavía no tenga ninguna reserva.
Si no existe: responde ok: true con encontrado: false y un mensaje (cliente nuevo → flujo de reserva normal). Las reservas canceladas no cuentan como visitas. En negocios de hostelería coches llega vacío; el historial y las próximas citas funcionan igual.
Sin teléfono (chat web): si el cliente escribe sin número y el teléfono llega vacío o sin dígitos (p. ej. el literal "None"), también responde ok: true con encontrado: false — nunca devuelve una ficha ajena ni un error.

Listar todas las reservas

GET /api/n/<slug>/reservas/ JWT requerido Lista completa de reservas del negocio
Requiere autenticación. Obtén el token primero con POST /api/auth/login/ y envíalo en la cabecera Authorization: Bearer <token>.
Respuesta
[ { "id": "550e8400-...", "ref": "PL-AB3X7K", "fecha": "2026-06-03", "hora": "09:15", "nombre": "Ana", "apellidos": "García", "telefono": "600111222", "servicio": "OPTIMUM — Txiki", "precio": "99 €", "modelo": "Audi A1 2023", "tiempo_estimado": "1h 10min", "estado": "activa" } ]

Casa rural — Habitaciones

Los negocios de tipo casa rural reservan ESTANCIAS (noches por habitación), no franjas horarias. La salida es exclusiva: el día de salida la habitación queda libre para otra entrada. Los mismos endpoints de reservas aceptan el payload de estancia que se documenta aquí.
GET /api/n/<slug>/habitaciones/ API Key / JWT Catálogo de habitaciones de la casa rural
Respuesta
{ "ok": true, "habitaciones": [ { "id": 1, "nombre": "Azul", "capacidad": 2, "precio_noche": 60.0, "descripcion": "Vistas al monte" } ], "noches_minimas": 2, "hora_checkin": "15:00", "hora_checkout": "12:00" }
precio_noche es el precio BASE de la habitación. Si el negocio define temporadas (p. ej. alta/baja), el precio real de cada noche depende de su fecha: usa siempre el precio_total que devuelve disponibilidad/ para el rango concreto en vez de multiplicar noches × precio_noche.

Casa rural — Registro de viajeros (Ertzaintza)

Si el alojamiento tiene activado el registro de alojados, la respuesta de reservas/crear/ incluye checkin_url: el enlace con el que el cliente registra a sus viajeros desde el móvil. Mándaselo en la conversación justo después de confirmar la reserva. Si el negocio no tiene la feature activa, el campo llega vacío o no aparece — en ese caso no menciones nada del registro.

En una reserva de grupo el enlace es uno para todas las habitaciones: no lo mandes varias veces.

El agente NO puede leer ni escribir los datos de los viajeros: son documentos de identidad y solo se abren con sesión del panel. Tu papel es entregar el enlace.
POST /api/n/<slug>/reservas/crear/ API Key / JWT Respuesta con el enlace de check-in
Respuesta (casa rural con registro activo)
{ "ok": true, "ref": "PL-ABC123", "id": "8f14e45f-…", "checkin_url": "https://checkin.wuama.app/checkin/hTPG60l0Ecmg…/", "noches": 3, "habitacion": "Azul", "precio": "285 €", "hora_checkin": "16:00", "hora_checkout": "11:00" }
Ejemplo de mensaje al cliente
Hola Marta 👋 Tu reserva en Casa Etxelu está confirmada: del 14 al 17 de agosto, habitación Azul. Por ley tenemos que registrar a los viajeros. Puedes hacerlo ahora desde el móvil en 2 minutos y a la llegada solo recogéis las llaves: 👉 https://checkin.wuama.app/checkin/hTPG60l0Ecmg…/ Ten a mano el DNI o pasaporte de cada persona mayor de edad.

Casa rural — Disponibilidad por noches

POST /api/n/<slug>/disponibilidad/ API Key / JWT Habitaciones libres para un rango de fechas
Petición
{ "fecha_entrada": "2026-09-11", "fecha_salida": "2026-09-13", "personas": 2 }
Respuesta
{ "ok": true, "noches": 2, "disponibles": [ { "id": 1, "nombre": "Azul", "capacidad": 2, "precio_noche": 60.0, "descripcion": "", "noches": 2, "precio_total": "215 €", "precio_total_num": 215.0, "desglose": [ { "temporada": null, "noches": 1, "precio_noche": 60.0 }, { "temporada": "Alta", "noches": 1, "precio_noche": 155.0 } ] } ], "hora_checkin": "15:00", "hora_checkout": "12:00", "fecha_entrada": "2026-09-11", "dia_semana_entrada": "viernes", "fecha_salida": "2026-09-13", "dia_semana_salida": "domingo" }
personas es opcional: si va, solo se ofrecen habitaciones donde caben. Si el rango no es reservable (mínimo de noches, festivo en la entrada, fecha pasada) la respuesta es ok: true con disponibles: [] y un mensaje para transmitir al cliente. Un festivo en mitad de la estancia NO la impide: solo cierra el día de entrada.
Temporadas: precio_total se calcula noche a noche — cada noche cobra la tarifa de su temporada si la habitación la tiene, y si no el precio_noche base. desglose solo aparece cuando alguna noche cobra tarifa de temporada (agrupa noches consecutivas a la misma tarifa; "temporada": null = tarifa base): úsalo para explicar el precio al cliente. Confirma SIEMPRE el precio con este precio_total, nunca multiplicando el precio base.

Casa rural — Crear / modificar estancia

POST /api/n/<slug>/reservas/crear/ API Key / JWT Crear una estancia (la habitación la elige el cliente)
Petición
{ "fecha_entrada": "2026-09-11", "fecha_salida": "2026-09-13", "habitacion": "Azul", "personas": 2, "nombre": "Ane", "apellidos": "Etxeberria", "telefono": "600111222" }
Respuesta
{ "ok": true, "ref": "PL-A1B2C3", "id": "…", "noches": 2, "habitacion": "Azul", "precio": "120 €", "hora_checkin": "15:00", "hora_checkout": "12:00", "fecha_entrada": "2026-09-11", "dia_semana_entrada": "viernes", "fecha_salida": "2026-09-13", "dia_semana_salida": "domingo" }
habitacion admite nombre o id. Si el precio no se manda, se calcula noche a noche (con la tarifa de temporada de cada noche si el negocio define temporadas; si no, noches × precio_noche). Rechazos de negocio (habitación ocupada, mínimo de noches, capacidad, entrada en festivo) → ok: false con el motivo en error. modificar/ acepta los mismos campos (fecha_entrada, fecha_salida, habitacion, personas…) y revalida rango y ocupación; buscar/, anular/ y citas/ funcionan igual que en taller (las citas incluyen fecha_fin, habitacion_nombre y noches).

Casa rural — Reserva de grupo

POST /api/n/<slug>/reservas/crear/ API Key / JWT Reservar VARIAS habitaciones en un solo alta
Petición
{ "fecha_entrada": "2026-09-11", "fecha_salida": "2026-09-13", "habitaciones": ["Azul", "Verde"], "personas": 4, "nombre": "Ane", "telefono": "600111222" }
Respuesta
{ "ok": true, "grupo_ref": "GR-A1B2C3", "noches": 2, "precio_total": "270 €", "personas": 4, "hora_checkin": "15:00", "hora_checkout": "12:00", "reservas": [ { "id": "…", "ref": "PL-AB12CD", "habitacion": "Azul", "precio": "120 €" }, { "id": "…", "ref": "PL-EF34GH", "habitacion": "Verde", "precio": "150 €" } ], "fecha_entrada": "2026-09-11", "dia_semana_entrada": "viernes", "fecha_salida": "2026-09-13", "dia_semana_salida": "domingo" }
Atómico: si alguna de las habitaciones pedidas no está libre en el rango, NO se reserva ninguna — la respuesta es ok: false con el nombre de la que falla en error. Cada habitación se crea como una reserva normal (con su ref y su precio) y todas comparten grupo_ref. Con una sola habitación en la lista se crea una estancia normal, sin grupo. personas es el total del GRUPO: no se valida contra la capacidad de cada habitación (4 personas pueden ir en una de 2 y otra de 3).
Anular: POST …/reservas/anular/ con {"grupo_ref": "GR-A1B2C3"} cancela TODAS las habitaciones del grupo (responde canceladas con cuántas); con {"id": "…"} cancela solo esa habitación y el resto del grupo sigue activo. Modificar siempre actúa sobre UNA reserva (una habitación), no sobre el grupo entero. En citas/ y clientes/consulta/ cada reserva del grupo lleva su grupo_ref.