Reservas en línea

Crafty gestiona citas para cualquier sitio que funcione por horas: barberías, salones, spas, estudios y clínicas. Los visitantes eligen un servicio, un profesional (o "quien esté libre") y una hora disponible. El checkout alojado de Crafty les pide sus datos y confirma la reserva. El negocio la ve en el calendario del panel, el cliente recibe una confirmación con un archivo de calendario y un recordatorio, y ambos pueden cancelar. Crafty no cobra comisión.

Los pagos son manuales por ahora: el cliente paga en el local o por transferencia bancaria. Los depósitos con tarjeta a través de la pasarela propia del negocio llegarán más adelante y se conectarán a las mismas reservas.

El frontend nunca decide qué horas están libres ni toca datos de pago. Muestra los servicios, pide a Crafty los horarios libres de un servicio y envía al visitante al checkout alojado con el horario que eligió, el cual queda reservado para él mientras completa sus datos.

Cómo encaja todo

Pieza Dónde Qué hace
Plan Central de Crafty (bookings_enabled) El plan del espacio de trabajo debe incluir reservas en línea. Es independiente de la tienda.
Ajustes Panel → sitio → Citas → Ajustes, o configure_bookings (administradores) Activa las reservas por entorno; zona horaria, horario semanal, fechas cerradas, aviso previo, con cuánta antelación, intervalo de horarios, ventana de cancelación, confirmación automática, recordatorio, métodos de pago, dirección, teléfono y los campos propios de la reserva.
Servicios La colección servicios Una entrada publicada por servicio, con su duración y precio.
Profesionales La colección profesionales Una entrada por persona que recibe reservas. Cada una puede tener su propio horario y bloqueos.
Página de reservas Tu frontend, con el SDK crafty.bookings.config() y crafty.bookings.availability(); crafty.checkout.start({ booking }).
Checkout checkout.crafty.do/{store}/c/{session} (alojado) Datos del cliente, campos de la reserva, método de pago, captcha; confirma la reserva.
Página de reserva para el cliente checkout.crafty.do/{store}/o/{number}?t={token} Fecha, hora, quién, precio, instrucciones de pago, añadir al calendario (.ics), cancelar. Enlazada desde cada correo.
Calendario Panel → Citas Vistas de día, semana y lista por profesional; crear, mover, confirmar, completar, no presentado, cancelar, marcar como pagada, nota interna, reenviar el correo; bloquear horas.
Integraciones Webhooks booking.created, booking.updated, booking.cancelled, booking.reminder_sent; la API autenticada y las herramientas MCP Sincronizan con otro calendario, un CRM o un TPV.

Cada reserva es también un pedido (kind: "booking"), así que comparte con la tienda los números de pedido, correos, impuestos, aspecto del checkout y analíticas.

1. Activa las reservas

En el panel: sitio → Citas → Ajustes → Activar citas. Mediante MCP (administradores):

configure_bookings {
  "site_id": 3,
  "enabled": true,
  "timezone": "America/Santo_Domingo",
  "hours": {
    "mon": [],
    "tue": [{ "start": "09:00", "end": "13:00" }, { "start": "14:00", "end": "19:00" }],
    "sat": [{ "start": "08:00", "end": "18:00" }],
    "sun": [{ "start": "09:00", "end": "13:00" }]
  },
  "closed_dates": ["2026-12-25"],
  "min_notice_minutes": 60,
  "max_days_ahead": 30,
  "slot_interval": 15,
  "cancel_notice_minutes": 120,
  "auto_confirm": true,
  "reminder_hours": 24,
  "payment_methods": { "pay_at_venue": true, "transfer": false },
  "address": "Calle El Conde 101, Zona Colonial",
  "phone": "809-555-0101"
}

Al activarlas:

  • crea las colecciones servicios y profesionales, o adopta las existentes con esos slugs cuando sus campos de reserva tengan los tipos indicados abajo (si no, informa de los campos en conflicto);
  • asigna al entorno una URL de checkout (el mismo identificador que la tienda);
  • conserva todos los ajustes que omitas. hours se combina por día de la semana: un día omitido mantiene su horario, y [] lo cierra; hasta 3 rangos por día.
Ajuste Valor por defecto Significado
timezone America/Santo_Domingo Todas las horas locales (horarios, franjas, correos) usan esta zona.
hours Lun–Sáb 09:00–18:00 El horario semanal de la tienda. El horario propio de un profesional siempre queda dentro de este.
min_notice_minutes 120 Con cuánta antelación mínima puede reservar un visitante antes de una franja.
max_days_ahead 30 Con cuánta antelación pueden reservar los visitantes (1–365).
slot_interval 15 Minutos entre las horas de inicio ofrecidas (5, 10, 15, 20, 30, 60).
cancel_notice_minutes 120 Hasta cuándo puede cancelar el cliente en línea (0 = hasta que empiece).
auto_confirm true false deja las reservas del sitio pendientes hasta que la tienda las confirme.
reminder_hours 24 Horas antes de la reserva para enviar un recordatorio por correo (0 lo desactiva).

Desactivarlas detiene las nuevas reservas; las existentes y las colecciones se mantienen. Envía configure_bookings solo con site_id para leer los ajustes actuales, los slugs de servicios y profesionales, y la URL del checkout.

Los correos de notificación, las cuentas para transferencias, la URL de retorno y el aspecto del checkout son ajustes de la tienda (configure_store), por lo que se comparten con los pedidos.

2. Servicios y profesionales

Mientras las reservas estén activas, estos campos pueden renombrarse pero no eliminarse ni cambiar de tipo, y las colecciones no pueden borrarse. Añade cualquier otro campo que necesites.

Colección Campo Tipo Notas
servicios nombre cadena, obligatorio Nombre (también origen del título y del slug).
duracion número, obligatorio Minutos que dura la reserva (5–720).
precio decimal, obligatorio Precio en la moneda de la tienda. Un servicio sin precio no se puede reservar.
impuesto cadena Una clave de impuesto de la tienda (itbis-18…). Si está vacío se usa el predeterminado.
imagenes multiReference → Media
profesionales multiReference → profesionales Quién lo realiza. Vacío significa todos.
margen número Minutos que quedan libres después (limpieza); nunca se muestran al cliente.
deposito decimal Depósito cuando lleguen los pagos con tarjeta.
profesionales nombre cadena, obligatorio
foto reference → Media
activo booleano false deja de aceptar reservas sin eliminar la entrada.

Solo los servicios publicados y los profesionales activos y publicados se pueden reservar. Una tienda sin profesionales también funciona: las reservas van a la tienda en conjunto, una a la vez.

El horario y los bloqueos de cada profesional (Panel → Citas → haz clic en su nombre, o MCP):

  • set_professional_schedule { site_id, professional: "luis", mode: "own", hours: { … } } le asigna su propio horario semanal; mode: "shop" vuelve al de la tienda.
  • manage_booking_blocks con action: "create" bloquea un rango una vez (from_date, to_date, all_day o start_time/end_time) o cada semana (repeat: "weekly", weekdays, horas), para un profesional o para toda la tienda. Responde cuántas reservas ya caen dentro (se mantienen; muévelas o cancélalas). action: "list" y action: "delete" leen y eliminan bloqueos.

3. La página de reservas en el frontend

import { createCraftyClient, CraftyError } from "@craftware/crafty-sdk";

const crafty = createCraftyClient({ baseUrl, apiKey, siteSlug: "navaja" });

const config = await crafty.bookings.config();
// { enabled, timezone, currency, services: [{ slug, name, duration_minutes, price, professionals }],
//   professionals: [{ slug, name, bio }], hours, closed_dates, min_notice_minutes, max_days_ahead, … }

const availability = await crafty.bookings.availability({
  service: "corte-y-barba",
  professional: "luis",      // omítelo para quien esté libre
  from: "2026-11-05",        // fechas locales; por defecto hoy → hoy + 6 días, como máximo 62 días
  to: "2026-11-11",
});
// { timezone, days: [{ date, closed, slots: [{ start: "2026-11-05T14:00:00-04:00", time: "14:00", professionals: ["luis"] }] }] }

Cuando el visitante elige una franja:

try {
  await crafty.checkout.start(
    { booking: { service: "corte-y-barba", professional: "luis", start: slot.start } },
    { returnUrl: `${location.origin}/gracias`, cancelUrl: location.href },
  );
} catch (error) {
  if (error instanceof CraftyError && error.code === "SLOT_UNAVAILABLE") {
    // Alguien la tomó mientras tanto: recarga las franjas y pide otra hora.
  }
}

checkout.start abre una sesión, reserva la franja durante 10 minutos (held_until) y envía el navegador al checkout alojado. checkout.create hace lo mismo sin redirigir. Si omites professional, Crafty asigna a quien esté libre en ese momento.

Reglas para la página:

  • Muestra config.services (son los reservables, con el precio que cobrará Crafty) y solo las franjas de availability(). Nunca calcules horarios libres en el frontend: el horario, los bloqueos, otras reservas, las retenciones, el aviso previo y los márgenes siempre cuentan.
  • Consulta las franjas en el navegador, nunca durante la compilación: una compilación estática mostraría horas que ya pasaron.
  • Muestra las horas locales de la zona horaria (slot.time, o slot.start con su desfase).
  • config.enabled === false significa que la tienda no acepta reservas: muestra el teléfono en su lugar.

La reserva de un cliente se puede consultar con el número y el token de su enlace: crafty.bookings.get(number, token).

Cupones de descuento

Los cupones son de la tienda (panel → Cupones, o upsert_coupon) con kinds que incluya "booking"; una tienda sin la sección de tienda encuentra Cupones dentro de Citas. Los cupones de porcentaje y de importe fijo descuentan del precio del servicio, con las mismas fechas, usos totales, usos por correo y mínimo; los cupones de envío gratis y de productos específicos nunca se aplican a un servicio. Pasa el código al checkout y el visitante verá allí el descuento, o por qué no se aplica, y podrá quitarlo o escribir otro:

const coupon = new URLSearchParams(location.search).get("cupon"); // un enlace compartido: /reservar?cupon=PRIMERA
await crafty.checkout.start({ booking: { service, professional, start: slot.start } }, { returnUrl, cancelUrl, coupon });

La reserva guarda discount_total y coupon_code; al cancelarla se devuelve el uso del cupón. Un cupón que no deja nada por pagar reserva con el método de pago none, ya pagada.

4. Checkout, correos y cancelación

El checkout alojado muestra el servicio, quién, cuándo y el precio, pide nombre, correo y teléfono, además de los campos propios de la reserva (checkout_fields de configure_bookings, con la misma forma que los de la tienda), un método de pago y el captcha, y confirma la reserva (o la deja pendiente si auto_confirm está desactivado). Después muestra la página de la reserva.

Correos, con el aspecto de la tienda: confirmación al cliente con un archivo de calendario, aviso a los correos de notificación de la tienda, el recordatorio reminder_hours antes, y un aviso cuando la reserva se mueva o cancele por cualquiera de las partes. El cliente cancela desde la página de la reserva hasta cancel_notice_minutes antes del inicio.

5. El calendario en el panel

Panel → sitio → Citas: vistas de día y semana con una columna por profesional, y una lista. Desde ahí el negocio crea reservas para clientes sin cita o llamadas telefónicas (pueden salir del horario con un aviso), las mueve por fecha, hora o profesional, cambia el estado (pending → confirmed → completed o no_show, o cancelled), las marca como pagadas, guarda una nota interna y reenvía el correo. Los editores necesitan la sección Citas (lectura para ver, escritura para cambiar).

Endpoints REST

Públicos

No se requiere clave de API; el sitio va en la URL. Los errores usan { error, message }.

Método Ruta Notas
GET /api/v1/sites/{site}/bookings Ajustes públicos, servicios y profesionales.
GET /api/v1/sites/{site}/bookings/availability service, professional?, from?, to? (fechas locales).
POST /api/v1/sites/{site}/bookings/checkout Cuerpo { service, professional?, start, fields?, return_url?, cancel_url?, coupon?, locale? } → 201 { data: { id, url, expires_at, held_until } }.
GET /api/v1/sites/{site}/bookings/{number} Token en ?token= o X-Order-Token.

Rechazos: SLOT_UNAVAILABLE (409), SERVICE_UNAVAILABLE, PROFESSIONAL_UNAVAILABLE, PAYMENT_METHOD_DISABLED, TOO_MANY_BOOKINGS, COUPON_INVALID (API de reservas y MCP), y BOOKINGS_DISABLED cuando las reservas están desactivadas.

API autenticada de reservas

Para otro calendario, un CRM o un TPV: un token OAuth (el mismo que MCP) con crafty:read para leer y crafty:write para crear o cambiar; los editores necesitan el permiso de reservas. Pasa site (slug) y, opcionalmente, environment (por defecto production) en la consulta o el cuerpo.

Método Ruta Notas
GET /api/v1/bookings Filtros from, to (fechas locales), status, professional (slug), search (número, nombre, correo, teléfono), metadata[crm_id]=… (hasta 5 pares), per_page (máx. 100).
POST /api/v1/bookings Cuerpo { service, professional?, start, customer: { name, email?, phone? }, payment_method?, fields?, metadata?, outside_hours?, internal_note?, coupon?, locale? } → 201.
GET /api/v1/bookings/{number} La reserva con next_statuses, internal_note, customer_url y events.
PATCH /api/v1/bookings/{number} Cualquiera de start, professional, outside_hours, status, paid, payment_reference, internal_note, metadata. Devuelve la reserva y applied.

Cada reserva incluye starts_at/ends_at en la zona horaria de la tienda, más starts_at_utc/ends_at_utc, y metadata (hasta 20 pares de cadenas) para vincularla con el registro del otro sistema.

Herramientas MCP

Herramienta Ámbito Qué hace
configure_bookings administradores Lee o cambia los ajustes anteriores.
get_booking_availability lectura Sin service: los servicios y profesionales. Con él: franjas libres por día.
list_bookings, get_booking lectura Mismos filtros y forma que la API.
create_booking escritura Una reserva para un cliente, por ejemplo tras una llamada telefónica.
update_booking escritura Mover, cambiar estado, marcar como pagada, nota, metadatos.
set_professional_schedule escritura El horario propio de un profesional o el de la tienda.
manage_booking_blocks escritura (listar: lectura) Bloquear tiempo una vez o cada semana.

Webhooks

Los webhooks de contenido (upsert_webhook) pueden escuchar booking.created, booking.updated (con changes: status, start, professional, payment, metadata), booking.cancelled y booking.reminder_sent. El data del cuerpo es la reserva (type: "booking") sin la nota interna. Las reservas también disparan los eventos de pedido (order.created, order.status_changed, order.paid), cuyos datos incluyen kind: "booking".

Plantilla inicial

npm create crafty -- --template=astro-booking crea un sitio de barbería en Astro: servicios con precio y duración, el equipo y una página /reservar (servicio → profesional → día y hora) que abre el checkout alojado. Crea las colecciones servicios y profesionales con servicios y barberos de demostración, y activa las reservas cuando el plan del espacio de trabajo las incluye.

Errores comunes

  • Horarios libres calculados o en caché en el frontend. Consulta siempre availability() en el navegador; Crafty vuelve a comprobar la franja cuando se abre el checkout.
  • Servicios en borrador o sin precio. Solo los servicios publicados con precio son reservables.
  • Todos hacen todos los servicios. Eso es lo que significa un campo profesionales vacío; complétalo para restringir un servicio a algunas personas.
  • Horario de un profesional fuera del de la tienda. Las franjas son la superposición: amplía primero el horario de la tienda.
  • Horas desfasadas. Configura timezone antes de aceptar reservas; las franjas y los correos lo usan.
  • Error 404 en el checkout. Las reservas están desactivadas, el plan no las incluye, o la sesión expiró: inicia una nueva.