Docs / API / Referencia

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:

ValorQué es
apiUrlLa URL base de la API (ver abajo).
slugEl identificador de tu negocio en Lumi, p. ej. mi-negocio.

URL base y versiones

http
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:

json
{
  "error": {
    "code": "invalid_request",
    "message": "Invalid request body.",
    "details": { }
  }
}

Los valores de code son estables; puedes ramificar tu lógica según ellos:

CódigoHTTPSignificado
invalid_request400Cuerpo malformado, campos faltantes o validación fallida.
forbidden403Origen no autorizado, o petición rechazada por la detección de bots.
not_found404El negocio o recurso referenciado no existe.
conflict409Petición duplicada (mismo cliente, servicio y horario en 30 s).
rate_limited429Demasiadas peticiones; reintenta más tarde.
internal_error500Error 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.

GET/businesses/:slug

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.

json · respuesta 200
{
  "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_..."
}
POST/businesses/:slug/available-dates

Devuelve las fechas con al menos un horario disponible para un servicio dentro de un rango. Útil para poblar el calendario de mes.

json · petición
{
  "serviceId": "uuid",
  "startDate": "2026-06-01",
  "endDate": "2026-07-31"
}
json · respuesta 200
{ "dates": ["2026-06-03", "2026-06-04", "2026-06-07"] }
POST/businesses/:slug/availability

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.

json · petición
{ "serviceId": "uuid", "date": "2026-06-03" }
json · respuesta 200
{
  "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
    }
  ]
}
POST/businesses/:slug/appointments

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.

json · petición
{
  "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).

json · respuesta 200
{
  "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).

POST/businesses/:slug/gift-cards

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.

json · petición
{
  "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).

POST/businesses/:slug/gift-cards/redeem

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.

json · petición
{ "code": "ABC-1234" }
json · respuesta 200
{
  "valid": true,
  "remaining_amount_cop": 75000,
  "reason": null
}

Cuando valid es false, reason trae una explicación legible ("Ya redimida", "Expirada", …).

POST/payments/:id/save-transaction

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").

json · petición
{
  "wompiTransactionId": "01_PROD_...",
  "referenceType": "appointment"
}
json · respuesta 200
{ "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ónPrueba
POST /businesses/:slug/appointmentsPOST /businesses/:slug/test/appointments
POST /businesses/:slug/gift-cardsPOST /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

  • /v1 es 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 /v1 sigue funcionando.
  • Los códigos de error son aditivos: trata cualquier código desconocido como un fallo genérico.