Referencia API
La API pública de reservas de Lumi te permite mostrar tu catálogo de servicios, consultar disponibilidad, crear reservas y vender tarjetas de regalo desde tu propio sitio web.
API pública v1. No necesitas una API key. Solo requieres la URL de la API y el identificador (slug) de tu negocio — ambos seguros para incluir en el navegador.
Antes de empezar
Para llamar a la API solo necesitas dos valores, ambos públicos:
| Valor | Qué es |
|---|---|
apiUrl | La URL base de la API (ver abajo). |
slug | El identificador de tu negocio en Lumi, p. ej. mi-negocio. |
URL base y versiones
Producción: https://app.lumiagenda.co/api/public/v1 Local (dev): http://localhost:3002/api/public/v1
El segmento /v1 es estable: nunca hacemos cambios incompatibles dentro de /v1. Las funciones nuevas se agregan como endpoints o campos opcionales; los cambios incompatibles van a /v2. El formato es JSON de entrada y salida, en UTF-8.
Autenticación y acceso
La API no usa API key ni tokens. El acceso se controla de forma automática, sin que envíes credenciales, mediante varias capas:
- Lista de dominios autorizados por negocio — solo los sitios que registres pueden llamar a la API en nombre de tu negocio. El propio dominio de Lumi siempre está permitido.
- Detección de bots en los endpoints que crean reservas o tarjetas de regalo, sin captcha visible.
- Límites de tasa en el borde de red para contener el tráfico abusivo (respuesta
429).
Para habilitar un dominio nuevo —por ejemplo el sitio propio de tu negocio— escríbenos y lo añadimos a tu lista de orígenes permitidos. En el navegador solo viven la URL de la API y el slug: ningún dato sensible.
Formato y errores
Las respuestas correctas son JSON con HTTP 200, salvo que se indique lo contrario. Cada error usa este sobre estándar:
{
"error": {
"code": "invalid_request",
"message": "Invalid request body.",
"details": { }
}
}Los valores de code son estables; puedes ramificar tu lógica según ellos:
| Código | HTTP | Significado |
|---|---|---|
invalid_request | 400 | Cuerpo malformado, campos faltantes o validación fallida. |
forbidden | 403 | Origen no autorizado, o petición rechazada por la detección de bots. |
not_found | 404 | El negocio o recurso referenciado no existe. |
conflict | 409 | Petición duplicada (mismo cliente, servicio y horario en 30 s). |
rate_limited | 429 | Demasiadas peticiones; reintenta más tarde. |
internal_error | 500 | Error del servidor. El mensaje es genérico; el detalle queda en los registros del servidor. |
Endpoints
Todas las rutas cuelgan de la URL base. Reemplaza :slug por el identificador de tu negocio. Los montos están en pesos colombianos (COP) — ver Manejo de montos.
Devuelve los datos del negocio, su catálogo de servicios activos y la llave pública de pagos necesaria para iniciar el checkout. Úsalo al cargar tu página de reservas.
{
"business": {
"id": "uuid",
"slug": "mi-negocio",
"name": "Casa Verde Wellness",
"logo_url": "https://.../logo.png",
"phone": "+57...",
"email": "hola@minegocio.com",
"website": "https://minegocio.com",
"instagram_url": "https://instagram.com/...",
"payment_requirement": "percentage",
"deposit_percentage": 30,
"deposit_fixed_amount": 0,
"currency": "COP"
},
"services": [
{
"id": "uuid",
"name": "Masaje 60 min",
"description": "Masaje relajante de cuerpo completo.",
"duration_minutes": 60,
"price": 150000,
"currency": "COP",
"category": "Masajes",
"photo_url": "https://...",
"staff_required_count": 1,
"require_room": true
}
],
"wompi_public_key": "pub_..."
}Devuelve las fechas con al menos un horario disponible para un servicio dentro de un rango. Útil para poblar el calendario de mes.
{
"serviceId": "uuid",
"startDate": "2026-06-01",
"endDate": "2026-07-31"
}{ "dates": ["2026-06-03", "2026-06-04", "2026-06-07"] }Devuelve los horarios disponibles para una fecha concreta, con el personal y las salas libres en cada uno. Los horarios vienen en la zona horaria del negocio.
{ "serviceId": "uuid", "date": "2026-06-03" }{
"slots": [
{
"start_time": "2026-06-03T09:00:00",
"end_time": "2026-06-03T10:00:00",
"available_staff_ids": ["uuid"],
"available_staff_names": ["Ana"],
"available_room_ids": ["uuid"],
"staff_count": 2
}
]
}Crea una reserva pendiente y devuelve los datos para el pago con Wompi. Si el negocio no exige pago (o una tarjeta de regalo cubre el total), la reserva se confirma automáticamente y payment_id llega como null. Protegido por detección de bots, lista de dominios e idempotencia de 30 s.
{
"serviceId": "uuid",
"startTime": "2026-06-03T09:00:00",
"customer": {
"name": "María García",
"phone": "+573001234567",
"email": "maria@example.com",
"dateOfBirth": "1990-05-12"
},
"giftCode": "ABC-1234",
"staffIds": ["uuid"],
"roomId": "uuid",
"payInFull": false
}Campos opcionales: email, giftCode, staffId / staffIds, roomId, payInFull. El teléfono va en formato E.164 con indicativo (+57 para Colombia).
{
"appointment_id": "uuid",
"payment_id": "uuid",
"amount_to_charge_cop": 45000,
"amount_in_cents": 4500000,
"wompi_public_key": "pub_...",
"wompi_reference": "appt_...",
"wompi_signature": "..."
}Errores típicos: invalid_request, conflict (reserva duplicada en 30 s), forbidden, not_found, internal_error (p. ej. el horario se acaba de ocupar).
Crea una tarjeta de regalo pendiente y devuelve los datos de pago de Wompi. El código de la tarjeta se genera únicamente cuando el pago queda confirmado.
{
"amount": 100000,
"purchaserName": "María García",
"purchaserEmail": "maria@example.com",
"purchaserPhone": "+573001234567",
"isGifted": true,
"recipientName": "Sofía López",
"recipientEmail": "sofia@example.com",
"recipientPhone": "+573009876543",
"message": "¡Feliz cumpleaños!",
"giftedServiceId": "uuid"
}El amount va en pesos. isGifted: true significa que la tarjeta es para el destinatario; false, para quien la compra. La respuesta tiene la misma forma que la de reservas (un id más los datos de pago de Wompi).
Valida un código de tarjeta de regalo y devuelve su saldo restante. Es de solo lectura: no descuenta el saldo. El saldo se aplica al pasar el código a /appointments como giftCode. Tiene su propio límite de tasa para evitar adivinar códigos.
{ "code": "ABC-1234" }{
"valid": true,
"remaining_amount_cop": 75000,
"reason": null
}Cuando valid es false, reason trae una explicación legible ("Ya redimida", "Expirada", …).
Tras volver del checkout de Wompi, la página de resultado envía aquí el transaction_id para que el servidor lo correlacione con la reserva o la tarjeta. :id es el UUID de la reserva (referenceType: "appointment") o del pago de la tarjeta ("gift_card").
{
"wompiTransactionId": "01_PROD_...",
"referenceType": "appointment"
}{ "ok": true, "data": { } }El resultado del pago lo decide el servidor. La confirmación de un pago ocurre en el servidor mediante un webhook firmado del proveedor de pagos — nunca desde el navegador. save-transaction solo mejora la experiencia mostrando el estado de inmediato.
Modo de prueba
Los endpoints que crean reservas o tarjetas tienen una variante paralela /test/* que fuerza el entorno de pruebas (sandbox) de pagos. Las filas que crean quedan marcadas como de prueba y se excluyen automáticamente de los reportes de ingresos.
| Producción | Prueba |
|---|---|
POST /businesses/:slug/appointments | POST /businesses/:slug/test/appointments |
POST /businesses/:slug/gift-cards | POST /businesses/:slug/test/gift-cards |
Los endpoints de solo lectura devuelven los mismos datos en ambos modos y no tienen variante /test/*.
Manejo de montos
Lumi guarda los montos en COP como pesos enteros, no en centavos. price: 150000 significa 150.000 COP. Los campos dirigidos a Wompi (como amount_in_cents) usan centavos (pesos × 100). No multipliques ni dividas por 100 por tu cuenta, salvo que hables directo con Wompi.
Idempotencia
POST /businesses/:slug/appointments tiene una guarda de idempotencia de 30 segundos: si reenvías una petición con el mismo negocio, servicio, teléfono y horario en menos de 30 s, recibes un 409 conflict. Esto evita el doble clic y los envíos duplicados. Para los demás endpoints, aplica un "debounce" en el cliente.
Política de versiones
/v1es estable. No hay cambios incompatibles una vez publicado.- Cambios aditivos (nuevos campos opcionales en la petición o la respuesta) no rompen y llegan directo a
/v1. Ignora los campos que no conozcas. - Cambios incompatibles (campos eliminados, rutas renombradas) van a
/v2, con aviso previo, mientras/v1sigue funcionando. - Los códigos de error son aditivos: trata cualquier código desconocido como un fallo genérico.