Online bookings

Crafty takes appointments for any site that works by the hour: barbershops, salons, spas, studios, clinics. Visitors pick a service, a professional (or "whoever is free") and a free time; Crafty's hosted checkout asks for their details and confirms the booking. The business sees it in the panel's calendar, the customer gets a confirmation with a calendar file and a reminder, and both can cancel. Crafty charges no commission.

Payments are manual for now: the customer pays at the venue or by bank transfer. Card deposits through the business's own gateway come later and plug into the same bookings.

The frontend never decides which times are free and never touches payment data. It lists services, asks Crafty for the free slots of one service, and sends the visitor to the hosted checkout with the slot they chose, which stays held for them while they fill in their details.

How it fits together

Piece Where What it does
Plan Crafty central (bookings_enabled) The workspace's plan must include online bookings. It is separate from the store.
Settings Panel → site → Citas → Ajustes, or configure_bookings (admins) Switch bookings on per environment; time zone, weekly hours, closed dates, notice, how far ahead, slot interval, cancellation window, auto-confirm, reminder, payment methods, address, phone, the booking's own fields.
Services The servicios collection One published entry per service, with its duration and price.
Professionals The profesionales collection One entry per person who takes bookings. Each can have their own hours and blocked times.
Booking page Your frontend, with the SDK crafty.bookings.config() and crafty.bookings.availability(); crafty.checkout.start({ booking }).
Checkout checkout.crafty.do/{store}/c/{session} (hosted) Customer details, the booking's fields, payment method, captcha; confirms the booking.
Booking page for the customer checkout.crafty.do/{store}/o/{number}?t={token} Date, time, who, price, payment instructions, add to calendar (.ics), cancel. Linked from every email.
Calendar Panel → Citas Day, week and list views per professional; create, move, confirm, complete, no-show, cancel, mark paid, internal note, resend the email; block time.
Integrations Webhooks booking.created, booking.updated, booking.cancelled, booking.reminder_sent; the authenticated API and the MCP tools Sync with another calendar, a CRM or a POS.

Every booking is also an order (kind: "booking"), so it shares the store's order numbers, emails, taxes, checkout look and analytics.

1. Switch bookings on

In the panel: site → Citas → Ajustes → Activar citas. Through MCP (admins):

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"
}

Switching it on:

  • creates the servicios and profesionales collections, or adopts existing ones with those slugs when their booking fields have the types below (otherwise it reports the conflicting fields);
  • gives the environment a checkout URL (the same handle as the store);
  • keeps every setting you leave out. hours merges by weekday: a day left out keeps its hours, [] closes it; up to 3 ranges per day.
Setting Default Meaning
timezone America/Santo_Domingo Every local time (hours, slots, emails) is in it.
hours Mon–Sat 09:00–18:00 The shop's weekly hours. A professional's own hours always fall inside them.
min_notice_minutes 120 The latest a visitor can book before a slot.
max_days_ahead 30 How far ahead visitors can book (1–365).
slot_interval 15 Minutes between offered start times (5, 10, 15, 20, 30, 60).
cancel_notice_minutes 120 Until when the customer can cancel online (0 = until it starts).
auto_confirm true false leaves site bookings pending until the shop confirms them.
reminder_hours 24 Hours before the booking to email a reminder (0 turns it off).

Switching it off stops new bookings; existing bookings and the collections stay. Send configure_bookings with only site_id to read the current settings, the service and professional slugs and the checkout URL.

Notification emails, the transfer accounts, the return URL and the checkout's look are the store's settings (configure_store), so they are shared with orders.

2. Services and professionals

While bookings are on, these fields can be relabeled but not removed or retyped, and the collections can't be deleted. Add any other fields you need.

Collection Field Type Notes
servicios nombre string, required Name (also the title and slug source).
duracion number, required Minutes the booking takes (5–720).
precio decimal, required Price in the store currency. A service without a price can't be booked.
impuesto string A tax key of the store (itbis-18…). Empty uses the default.
imagenes multiReference → Media
profesionales multiReference → profesionales Who does it. Empty means everyone.
margen number Minutes kept free after it (cleanup); never shown to the customer.
deposito decimal Deposit when card payments arrive.
profesionales nombre string, required
foto reference → Media
activo boolean false stops taking bookings without deleting the entry.

Only published services and active, published professionals are bookable. A shop without professionals still works: bookings go to the shop as a whole, one at a time.

Each professional's hours and blocks (Panel → Citas → click their name, or MCP):

  • set_professional_schedule { site_id, professional: "luis", mode: "own", hours: { … } } gives them their own weekly hours; mode: "shop" goes back to the shop's.
  • manage_booking_blocks with action: "create" blocks a range once (from_date, to_date, all_day or start_time/end_time) or every week (repeat: "weekly", weekdays, times), for one professional or the whole shop. It answers how many bookings already fall inside (they stay; move or cancel them). action: "list" and action: "delete" read and remove blocks.

3. The booking page in the 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",      // leave it out for whoever is free
  from: "2026-11-05",        // local dates; default today → today + 6 days, at most 62 days
  to: "2026-11-11",
});
// { timezone, days: [{ date, closed, slots: [{ start: "2026-11-05T14:00:00-04:00", time: "14:00", professionals: ["luis"] }] }] }

When the visitor picks a slot:

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") {
    // Someone took it meanwhile: reload the slots and ask for another time.
  }
}

checkout.start opens a session, holds the slot for 10 minutes (held_until) and sends the browser to the hosted checkout. checkout.create does the same without redirecting. With professional left out, Crafty assigns whoever is free at that time.

Rules for the page:

  • Show config.services (they are the bookable ones, with the price Crafty will charge) and only the slots from availability(). Never compute free times in the frontend: hours, blocks, other bookings, holds, notice and buffers all count.
  • Fetch slots in the browser, never at build time: a static build would show times that are long gone.
  • Show the time zone's local times (slot.time, or slot.start with its offset).
  • config.enabled === false means the shop isn't taking bookings: show the phone instead.

A customer's booking can be read with the number and the token from their link: crafty.bookings.get(number, token).

Discount coupons

Coupons are the store's (panel → Cupones, or upsert_coupon) with kinds including "booking"; a shop without the store finds Cupones under Citas. Percent and fixed coupons take their discount off the service price, with the same dates, total uses, uses per email and minimum; free-delivery and specific-product coupons never apply to a service. Pass the code to the checkout and the visitor sees the discount there, or why it doesn't apply, and can remove it or type another:

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

The booking keeps discount_total and coupon_code; cancelling it gives the use back. A coupon that leaves nothing to pay books with payment method none, already paid.

4. Checkout, emails and cancellation

The hosted checkout shows the service, who, when and the price, asks for name, email and phone plus the booking's own fields (checkout_fields of configure_bookings, same shape as the store's), a payment method and the captcha, and confirms the booking (or leaves it pending when auto_confirm is off). It then shows the booking page.

Emails, with the store's look: confirmation to the customer with a calendar file, a notice to the store's notify emails, the reminder reminder_hours before, and a notice when the booking is moved or cancelled by either side. The customer cancels from the booking page until cancel_notice_minutes before the start.

5. The calendar in the panel

Panel → site → Citas: day and week views with a column per professional, and a list. From there the business creates bookings for walk-ins or phone calls (they may go outside the hours with a warning), moves them by date, time or professional, changes the status (pending → confirmed → completed or no_show, or cancelled), marks them paid, keeps an internal note and resends the email. Editors need the Citas section (read to see, write to change).

REST endpoints

Public

No API key is required; the site is in the URL. Errors use { error, message }.

Method Path Notes
GET /api/v1/sites/{site}/bookings Public settings, services and professionals.
GET /api/v1/sites/{site}/bookings/availability service, professional?, from?, to? (local dates).
POST /api/v1/sites/{site}/bookings/checkout Body { 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 in ?token= or X-Order-Token.

Rejections: SLOT_UNAVAILABLE (409), SERVICE_UNAVAILABLE, PROFESSIONAL_UNAVAILABLE, PAYMENT_METHOD_DISABLED, TOO_MANY_BOOKINGS, COUPON_INVALID (bookings API and MCP), and BOOKINGS_DISABLED when bookings are off.

Authenticated bookings API

For another calendar, a CRM or a POS: an OAuth token (the same as MCP) with crafty:read to read and crafty:write to create or change; editors need the bookings permission. Pass site (slug) and optionally environment (default production) in the query or body.

Method Path Notes
GET /api/v1/bookings Filters from, to (local dates), status, professional (slug), search (number, name, email, phone), metadata[crm_id]=… (up to 5 pairs), per_page (max 100).
POST /api/v1/bookings Body { service, professional?, start, customer: { name, email?, phone? }, payment_method?, fields?, metadata?, outside_hours?, internal_note?, coupon?, locale? } → 201.
GET /api/v1/bookings/{number} The booking with next_statuses, internal_note, customer_url and events.
PATCH /api/v1/bookings/{number} Any of start, professional, outside_hours, status, paid, payment_reference, internal_note, metadata. Returns the booking and applied.

Each booking carries starts_at/ends_at in the shop's time zone plus starts_at_utc/ends_at_utc, and metadata (up to 20 string pairs) to tie it to the other system's record.

MCP tools

Tool Scope What it does
configure_bookings admins Read or change the settings above.
get_booking_availability read Without service: the services and professionals. With it: free slots per day.
list_bookings, get_booking read Same filters and shape as the API.
create_booking write A booking for a customer, e.g. from a phone call.
update_booking write Move, change status, mark paid, note, metadata.
set_professional_schedule write A professional's own hours or the shop's.
manage_booking_blocks write (list: read) Block time once or weekly.

Webhooks

Content webhooks (upsert_webhook) can listen to booking.created, booking.updated (with changes: status, start, professional, payment, metadata), booking.cancelled and booking.reminder_sent. The body's data is the booking (type: "booking") without the internal note. Bookings also fire the order events (order.created, order.status_changed, order.paid), whose data includes kind: "booking".

Starter

npm create crafty -- --template=astro-booking creates a barbershop site in Astro: services with price and duration, the team, and a /reservar page (service → professional → day and time) that opens the hosted checkout. It creates the servicios and profesionales collections with demo services and barbers, and switches bookings on when the workspace plan includes them.

Common pitfalls

  • Free times computed or cached in the frontend. Always ask availability() in the browser; Crafty checks the slot again when the checkout opens.
  • Services as drafts or without a price. Only published services with a price are bookable.
  • Everyone does every service. That's what an empty profesionales field means; fill it to restrict a service to some people.
  • A professional's hours outside the shop's. Slots are the overlap: widen the shop's hours first.
  • Times off by hours. Set timezone before taking bookings; slots and emails use it.
  • Checkout 404. Bookings are off, the plan doesn't include them, or the session expired: start a new one.