@craftware/crafty-sdk 0.11.0 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,221 @@
1
+ # Online bookings
2
+
3
+ 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.
4
+
5
+ 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.
6
+
7
+ 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.
8
+
9
+ ## How it fits together
10
+
11
+ | Piece | Where | What it does |
12
+ | --- | --- | --- |
13
+ | Plan | Crafty central (`bookings_enabled`) | The workspace's plan must include online bookings. It is separate from the store. |
14
+ | 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. |
15
+ | Services | The `servicios` collection | One published entry per service, with its duration and price. |
16
+ | Professionals | The `profesionales` collection | One entry per person who takes bookings. Each can have their own hours and blocked times. |
17
+ | Booking page | Your frontend, with the SDK | `crafty.bookings.config()` and `crafty.bookings.availability()`; `crafty.checkout.start({ booking })`. |
18
+ | Checkout | `checkout.crafty.do/{store}/c/{session}` (hosted) | Customer details, the booking's fields, payment method, captcha; confirms the booking. |
19
+ | 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. |
20
+ | 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. |
21
+ | 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. |
22
+
23
+ Every booking is also an order (`kind: "booking"`), so it shares the store's order numbers, emails, taxes, checkout look and analytics.
24
+
25
+ ## 1. Switch bookings on
26
+
27
+ In the panel: site → **Citas** → **Ajustes** → **Activar citas**. Through MCP (admins):
28
+
29
+ ```json
30
+ configure_bookings {
31
+ "site_id": 3,
32
+ "enabled": true,
33
+ "timezone": "America/Santo_Domingo",
34
+ "hours": {
35
+ "mon": [],
36
+ "tue": [{ "start": "09:00", "end": "13:00" }, { "start": "14:00", "end": "19:00" }],
37
+ "sat": [{ "start": "08:00", "end": "18:00" }],
38
+ "sun": [{ "start": "09:00", "end": "13:00" }]
39
+ },
40
+ "closed_dates": ["2026-12-25"],
41
+ "min_notice_minutes": 60,
42
+ "max_days_ahead": 30,
43
+ "slot_interval": 15,
44
+ "cancel_notice_minutes": 120,
45
+ "auto_confirm": true,
46
+ "reminder_hours": 24,
47
+ "payment_methods": { "pay_at_venue": true, "transfer": false },
48
+ "address": "Calle El Conde 101, Zona Colonial",
49
+ "phone": "809-555-0101"
50
+ }
51
+ ```
52
+
53
+ Switching it on:
54
+
55
+ - 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);
56
+ - gives the environment a checkout URL (the same handle as the store);
57
+ - 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.
58
+
59
+ | Setting | Default | Meaning |
60
+ | --- | --- | --- |
61
+ | `timezone` | `America/Santo_Domingo` | Every local time (hours, slots, emails) is in it. |
62
+ | `hours` | Mon–Sat 09:00–18:00 | The shop's weekly hours. A professional's own hours always fall inside them. |
63
+ | `min_notice_minutes` | 120 | The latest a visitor can book before a slot. |
64
+ | `max_days_ahead` | 30 | How far ahead visitors can book (1–365). |
65
+ | `slot_interval` | 15 | Minutes between offered start times (5, 10, 15, 20, 30, 60). |
66
+ | `cancel_notice_minutes` | 120 | Until when the customer can cancel online (0 = until it starts). |
67
+ | `auto_confirm` | `true` | `false` leaves site bookings pending until the shop confirms them. |
68
+ | `reminder_hours` | 24 | Hours before the booking to email a reminder (0 turns it off). |
69
+
70
+ 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.
71
+
72
+ 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.
73
+
74
+ ## 2. Services and professionals
75
+
76
+ 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.
77
+
78
+ | Collection | Field | Type | Notes |
79
+ | --- | --- | --- | --- |
80
+ | `servicios` | `nombre` | string, required | Name (also the title and slug source). |
81
+ | | `duracion` | number, required | Minutes the booking takes (5–720). |
82
+ | | `precio` | decimal, required | Price in the store currency. A service without a price can't be booked. |
83
+ | | `impuesto` | string | A tax key of the store (`itbis-18`…). Empty uses the default. |
84
+ | | `imagenes` | multiReference → Media | |
85
+ | | `profesionales` | multiReference → `profesionales` | Who does it. **Empty means everyone.** |
86
+ | | `margen` | number | Minutes kept free after it (cleanup); never shown to the customer. |
87
+ | | `deposito` | decimal | Deposit when card payments arrive. |
88
+ | `profesionales` | `nombre` | string, required | |
89
+ | | `foto` | reference → Media | |
90
+ | | `activo` | boolean | `false` stops taking bookings without deleting the entry. |
91
+
92
+ 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.
93
+
94
+ Each professional's hours and blocks (Panel → Citas → click their name, or MCP):
95
+
96
+ - `set_professional_schedule { site_id, professional: "luis", mode: "own", hours: { … } }` gives them their own weekly hours; `mode: "shop"` goes back to the shop's.
97
+ - `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.
98
+
99
+ ## 3. The booking page in the frontend
100
+
101
+ ```ts
102
+ import { createCraftyClient, CraftyError } from "@craftware/crafty-sdk";
103
+
104
+ const crafty = createCraftyClient({ baseUrl, apiKey, siteSlug: "navaja" });
105
+
106
+ const config = await crafty.bookings.config();
107
+ // { enabled, timezone, currency, services: [{ slug, name, duration_minutes, price, professionals }],
108
+ // professionals: [{ slug, name, bio }], hours, closed_dates, min_notice_minutes, max_days_ahead, … }
109
+
110
+ const availability = await crafty.bookings.availability({
111
+ service: "corte-y-barba",
112
+ professional: "luis", // leave it out for whoever is free
113
+ from: "2026-11-05", // local dates; default today → today + 6 days, at most 62 days
114
+ to: "2026-11-11",
115
+ });
116
+ // { timezone, days: [{ date, closed, slots: [{ start: "2026-11-05T14:00:00-04:00", time: "14:00", professionals: ["luis"] }] }] }
117
+ ```
118
+
119
+ When the visitor picks a slot:
120
+
121
+ ```ts
122
+ try {
123
+ await crafty.checkout.start(
124
+ { booking: { service: "corte-y-barba", professional: "luis", start: slot.start } },
125
+ { returnUrl: `${location.origin}/gracias`, cancelUrl: location.href },
126
+ );
127
+ } catch (error) {
128
+ if (error instanceof CraftyError && error.code === "SLOT_UNAVAILABLE") {
129
+ // Someone took it meanwhile: reload the slots and ask for another time.
130
+ }
131
+ }
132
+ ```
133
+
134
+ `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.
135
+
136
+ Rules for the page:
137
+
138
+ - 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.
139
+ - Fetch slots in the browser, never at build time: a static build would show times that are long gone.
140
+ - Show the time zone's local times (`slot.time`, or `slot.start` with its offset).
141
+ - `config.enabled === false` means the shop isn't taking bookings: show the phone instead.
142
+
143
+ A customer's booking can be read with the number and the token from their link: `crafty.bookings.get(number, token)`.
144
+
145
+ ### Discount coupons
146
+
147
+ 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:
148
+
149
+ ```ts
150
+ const coupon = new URLSearchParams(location.search).get("cupon"); // a shared link: /reservar?cupon=PRIMERA
151
+ await crafty.checkout.start({ booking: { service, professional, start: slot.start } }, { returnUrl, cancelUrl, coupon });
152
+ ```
153
+
154
+ 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.
155
+
156
+ ## 4. Checkout, emails and cancellation
157
+
158
+ 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.
159
+
160
+ 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.
161
+
162
+ ## 5. The calendar in the panel
163
+
164
+ 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).
165
+
166
+ ## REST endpoints
167
+
168
+ ### Public
169
+
170
+ No API key is required; the site is in the URL. Errors use `{ error, message }`.
171
+
172
+ | Method | Path | Notes |
173
+ | --- | --- | --- |
174
+ | GET | `/api/v1/sites/{site}/bookings` | Public settings, services and professionals. |
175
+ | GET | `/api/v1/sites/{site}/bookings/availability` | `service`, `professional?`, `from?`, `to?` (local dates). |
176
+ | 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 } }`. |
177
+ | GET | `/api/v1/sites/{site}/bookings/{number}` | Token in `?token=` or `X-Order-Token`. |
178
+
179
+ 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.
180
+
181
+ ### Authenticated bookings API
182
+
183
+ 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.
184
+
185
+ | Method | Path | Notes |
186
+ | --- | --- | --- |
187
+ | 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). |
188
+ | POST | `/api/v1/bookings` | Body `{ service, professional?, start, customer: { name, email?, phone? }, payment_method?, fields?, metadata?, outside_hours?, internal_note?, coupon?, locale? }` → 201. |
189
+ | GET | `/api/v1/bookings/{number}` | The booking with `next_statuses`, `internal_note`, `customer_url` and `events`. |
190
+ | PATCH | `/api/v1/bookings/{number}` | Any of `start`, `professional`, `outside_hours`, `status`, `paid`, `payment_reference`, `internal_note`, `metadata`. Returns the booking and `applied`. |
191
+
192
+ 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.
193
+
194
+ ## MCP tools
195
+
196
+ | Tool | Scope | What it does |
197
+ | --- | --- | --- |
198
+ | `configure_bookings` | admins | Read or change the settings above. |
199
+ | `get_booking_availability` | read | Without `service`: the services and professionals. With it: free slots per day. |
200
+ | `list_bookings`, `get_booking` | read | Same filters and shape as the API. |
201
+ | `create_booking` | write | A booking for a customer, e.g. from a phone call. |
202
+ | `update_booking` | write | Move, change status, mark paid, note, metadata. |
203
+ | `set_professional_schedule` | write | A professional's own hours or the shop's. |
204
+ | `manage_booking_blocks` | write (list: read) | Block time once or weekly. |
205
+
206
+ ## Webhooks
207
+
208
+ 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"`.
209
+
210
+ ## Starter
211
+
212
+ `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.
213
+
214
+ ## Common pitfalls
215
+
216
+ - **Free times computed or cached in the frontend.** Always ask `availability()` in the browser; Crafty checks the slot again when the checkout opens.
217
+ - **Services as drafts or without a price.** Only published services with a price are bookable.
218
+ - **Everyone does every service.** That's what an empty `profesionales` field means; fill it to restrict a service to some people.
219
+ - **A professional's hours outside the shop's.** Slots are the overlap: widen the shop's hours first.
220
+ - **Times off by hours.** Set `timezone` before taking bookings; slots and emails use it.
221
+ - **Checkout 404.** Bookings are off, the plan doesn't include them, or the session expired: start a new one.