VitrinaAPI

Mostrar tus horas libres en tu sitio y recibir la reserva

Consulta disponibilidad y reserva desde tu propio servidor, sin widget

Tu propio sitio puede mostrar las horas libres del workspace y dejar que alguien reserve, sin abrir la aplicación. Esta receta cubre las dos mitades: qué horas están disponibles y cómo se confirma la reserva.

Trampa

No hay una versión de esta receta segura para el navegador: agenda y reserva se llaman desde tu servidor, siempre

La key publicable (pk_), la misma que usa el chat insertable del sitio, no sirve aquí: no incluye appointments:read ni appointments:write. Usa una sk_ o un token personal, siempre desde tu servidor y nunca en el navegador.

Antes de empezar

  • appointment_types:read y appointment_types:write para definir qué se puede reservar.
  • appointments:read para consultar horario y disponibilidad.
  • appointments:write para confirmar una reserva; appointments:delete más messages:send para cancelarla.
  • Familias de credencial en Autenticación.

1. Define qué se puede reservar

curl -X POST https://api.vitrinadev.com/api/v1/appointment-types \
  -H "Authorization: Bearer $VITRINA_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Reunión inicial",
    "description": "Primera conversación para entender lo que necesitas.",
    "kind": "external",
    "duration_minutes": 30
  }'
{
  "data": {
    "id": "eed63545-0f09-47a3-9d14-d844ba1b0cd1",
    "name": "Reunion inicial",
    "kind": "external",
    "duration_minutes": 30,
    "price_amount": null,
    "eligible_staff_ids": [],
    "is_active": true
  }
}

No hay un campo de color ni de ubicación aquí: la ubicación se manda por reserva, no por tipo. duration_minutes fija el largo del bloque. eligible_staff_ids vacío significa que cualquiera del equipo puede tomarla; con la lista llena, la reserva rota solo entre esos nombres. price_amount en null se lee como «a consultar»; en 0, como gratis. No son lo mismo.

2. Revisa la política horaria antes de mostrar nada

curl https://api.vitrinadev.com/api/v1/appointments/config \
  -H "Authorization: Bearer $VITRINA_KEY"
{ "data": { "configured": true, "timezone": "America/Santiago" } }

configured indica si el workspace guardó una política horaria. Mientras sea false, la agenda igual funciona con el horario general del workspace. Guardar horario, buffer, capacidad y el resto del detalle está en Agenda y citas; aquí solo se lee.

3. Pide las horas libres

curl "https://api.vitrinadev.com/api/v1/appointments/availability?appointment_type_id=eed63545-0f09-47a3-9d14-d844ba1b0cd1" \
  -H "Authorization: Bearer $VITRINA_KEY"
{
  "data": {
    "configured": true,
    "timezone": "America/Santiago",
    "slots": [
      { "startsAt": "2026-09-23T14:00:00.000Z", "endsAt": "2026-09-23T14:30:00.000Z", "label": "2026-09-23 11:00" },
      { "startsAt": "2026-09-23T14:30:00.000Z", "endsAt": "2026-09-23T15:00:00.000Z", "label": "2026-09-23 11:30" }
    ]
  }
}

appointment_type_id fija el largo de cada bloque según la duración del tipo. La ventana tiene un piso y un techo fijos, sin importar qué from/to mandes. Nunca empieza antes del tiempo mínimo de aviso. Nunca pasa el horizonte de reserva del workspace.

La respuesta queda en caché por unos treinta segundos. Un bloque mostrado aquí puede estar tomado para cuando llegues al paso 4. Eso no produce una reserva doblada: el paso 4 vuelve a validar la hora contra la agenda real. Como mucho, produce un rechazo limpio.

4. Recibe la reserva

curl -X POST https://api.vitrinadev.com/api/v1/appointments \
  -H "Authorization: Bearer $VITRINA_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "starts_at": "2026-09-24T14:00:00.000Z",
    "ends_at": "2026-09-24T14:30:00.000Z",
    "kind": "external",
    "appointment_type_id": "eed63545-0f09-47a3-9d14-d844ba1b0cd1",
    "owner_user_id": "3197957f-5fb6-4c7a-837d-1fe296bc548d",
    "customer_name": "Camila Rios"
  }'
{
  "data": {
    "id": "a06600c8-7577-42dd-81c0-24392ed77976",
    "status": "confirmed",
    "starts_at": "2026-09-24T14:00:00.000Z",
    "ends_at": "2026-09-24T14:30:00.000Z",
    "customer_name": "Camila Rios",
    "display_id": "A-14"
  }
}

Un 201 aquí es una reserva confirmada, sin un paso de confirmación aparte. También agenda el recordatorio y refleja el evento en el calendario compartido cuando hay uno conectado, sin bloquear la respuesta. Reservado como operador, el horario de atención y el aviso mínimo no aplican aquí. Esos límites los exige solo la IA cuando reserva por su cuenta. Deja owner_user_id fuera y la reserva rota entre el personal elegible del tipo.

En la aplicación: el mismo calendario, con arrastrar y soltar, vive en Agenda. El catálogo de tipos de cita está en Configuración → Negocio → Tipos de cita. El agendamiento dentro del chat del sitio, con su propia key pk_, se activa aparte en Configuración → Canales → Web chat, y no pasa por esta receta. Guía completa en Manual de plataforma → Mostrar tus horas libres y recibir la reserva.

5. Cancela sin dejar cabos sueltos

curl -X POST https://api.vitrinadev.com/api/v1/appointments/c6f1bf05-8d32-4ff9-a01e-6616995652a1/cancel \
  -H "Authorization: Bearer $VITRINA_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "reason": "El cliente reagendó por teléfono." }'
{ "data": { "id": "c6f1bf05-8d32-4ff9-a01e-6616995652a1", "status": "cancelled" } }

Cancelar exige appointments:delete y messages:send a la vez, un scope propio y no un permiso más amplio. Libera el bloque, borra el evento del calendario compartido y le escribe al cliente. Ese aviso no se puede deshacer, así que un rol puede tener permiso para reservar y reagendar sin tener permiso para cancelar.

Cuando falla

Dos reservas sobre el mismo bloque, con el mismo owner_user_id, responden un 409 con la razón en details.reason:

{ "error": { "code": "CONFLICT", "message": "Could not book appointment: professional_taken", "details": { "reason": "professional_taken" } } }

professional_taken no es lo mismo que slot_taken. professional_taken es la persona nombrada la que ya no tiene cupo en ese bloque; slot_taken es la capacidad general del bloque la que se agotó, sin nombrar a nadie. Las dos son carreras perdibles: la recuperación es ofrecer otro bloque, nunca reintentar el mismo. Un problema con la solicitud misma, en cambio, responde 400 con razón invalid: horas que no cuadran, o un appointment_type_id desconocido o inactivo.

Los eventos appointment.booked, .rescheduled, .cancelled y .no_show dejan ver el mismo ciclo desde tu propio sistema, sin sondear la API; el catálogo completo está en Webhooks.

En esta página