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
serviciosyprofesionales, 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.
hoursse 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_blocksconaction: "create"bloquea un rango una vez (from_date,to_date,all_dayostart_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"yaction: "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 deavailability(). 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, oslot.startcon su desfase). config.enabled === falsesignifica 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
profesionalesvací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
timezoneantes 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.