@proveanything/smartlinks 2.1.1 → 2.1.3
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.
- package/dist/api/bookings.d.ts +7 -1
- package/dist/api/bookings.js +9 -0
- package/dist/docs/API_SUMMARY.md +34 -1
- package/dist/docs/app-manifest.md +19 -2
- package/dist/docs/bookings.md +75 -2
- package/dist/docs/deep-link-discovery.md +12 -0
- package/dist/docs/iframe-responder.md +1 -0
- package/dist/docs/server-functions.md +13 -1
- package/dist/docs/websites.md +26 -0
- package/dist/iframeResponder.d.ts +2 -0
- package/dist/iframeResponder.js +18 -8
- package/dist/openapi.yaml +90 -0
- package/dist/testing/bookingsMemory.d.ts +11 -0
- package/dist/testing/bookingsMemory.js +22 -0
- package/dist/testing/index.js +1 -1
- package/dist/types/appManifest.d.ts +19 -5
- package/dist/types/bookings.d.ts +31 -0
- package/dist/types/iframeResponder.d.ts +6 -0
- package/docs/API_SUMMARY.md +34 -1
- package/docs/app-manifest.md +19 -2
- package/docs/bookings.md +75 -2
- package/docs/deep-link-discovery.md +12 -0
- package/docs/iframe-responder.md +1 -0
- package/docs/server-functions.md +13 -1
- package/docs/websites.md +26 -0
- package/openapi.yaml +90 -0
- package/package.json +1 -1
package/dist/api/bookings.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Booking, BookingSlot, BookingAllocation, CreateBookingInput, UpdateBookingInput, MoveBookingInput, BookingListQueryParams, CreateSlotInput, UpdateSlotInput, SlotListQueryParams, CreateBlockInput, BlockListQueryParams, FreeBusyQuery, FreeBusyResponse, BookingAccessOptions } from '../types/bookings.js';
|
|
1
|
+
import type { Booking, BookingSlot, BookingAllocation, CreateBookingInput, UpdateBookingInput, MoveBookingInput, BookingListQueryParams, CreateSlotInput, UpdateSlotInput, SlotListQueryParams, CreateBlockInput, BlockListQueryParams, FreeBusyQuery, FreeBusyResponse, BookingAccessOptions, MessageBookersInput, MessageBookersResult } from '../types/bookings.js';
|
|
2
2
|
import type { PaginatedResponse } from '../types/appObjects.js';
|
|
3
3
|
export declare namespace bookings {
|
|
4
4
|
/**
|
|
@@ -55,6 +55,12 @@ export declare namespace bookings {
|
|
|
55
55
|
* GET /bookings/freebusy
|
|
56
56
|
*/
|
|
57
57
|
function freebusy(collectionId: string, appId: string, q: FreeBusyQuery, admin?: boolean): Promise<FreeBusyResponse>;
|
|
58
|
+
/**
|
|
59
|
+
* Message the people booked (admin): a template to everyone on a slot, or everyone booked in a date
|
|
60
|
+
* range (optionally with one person / room), each send hydrated with their booking ({{ booking }}).
|
|
61
|
+
* No segment to make. POST /bookings/message
|
|
62
|
+
*/
|
|
63
|
+
function message(collectionId: string, appId: string, input: MessageBookersInput): Promise<MessageBookersResult>;
|
|
58
64
|
namespace slots {
|
|
59
65
|
/** Create a slot (admin). POST /bookings/slots */
|
|
60
66
|
function create(collectionId: string, appId: string, input: CreateSlotInput): Promise<BookingSlot>;
|
package/dist/api/bookings.js
CHANGED
|
@@ -122,6 +122,15 @@ export var bookings;
|
|
|
122
122
|
return request(`${basePath(collectionId, appId, admin)}/freebusy${query(params)}`);
|
|
123
123
|
}
|
|
124
124
|
bookings.freebusy = freebusy;
|
|
125
|
+
/**
|
|
126
|
+
* Message the people booked (admin): a template to everyone on a slot, or everyone booked in a date
|
|
127
|
+
* range (optionally with one person / room), each send hydrated with their booking ({{ booking }}).
|
|
128
|
+
* No segment to make. POST /bookings/message
|
|
129
|
+
*/
|
|
130
|
+
async function message(collectionId, appId, input) {
|
|
131
|
+
return post(`${basePath(collectionId, appId, true)}/message`, input);
|
|
132
|
+
}
|
|
133
|
+
bookings.message = message;
|
|
125
134
|
// ==================== SLOTS ====================
|
|
126
135
|
let slots;
|
|
127
136
|
(function (slots_1) {
|
package/dist/docs/API_SUMMARY.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Smartlinks API Summary
|
|
2
2
|
|
|
3
|
-
Version: 2.1.
|
|
3
|
+
Version: 2.1.3 | Generated: 2026-10-26T21:00:54.277Z
|
|
4
4
|
|
|
5
5
|
This is a concise summary of all available API functions and types.
|
|
6
6
|
|
|
@@ -2585,9 +2585,11 @@ interface AppContainerComponent {
|
|
|
2585
2585
|
**DeepLinkEntry** (interface)
|
|
2586
2586
|
```typescript
|
|
2587
2587
|
interface DeepLinkEntry {
|
|
2588
|
+
id?: string;
|
|
2588
2589
|
title: string;
|
|
2589
2590
|
path?: string;
|
|
2590
2591
|
params?: Record<string, string>;
|
|
2592
|
+
accepts?: PublicViewParams;
|
|
2591
2593
|
widget?: true;
|
|
2592
2594
|
}
|
|
2593
2595
|
```
|
|
@@ -2816,6 +2818,7 @@ interface ServerFunctionBookings {
|
|
|
2816
2818
|
complete(bookingId: string): Promise<Booking>;
|
|
2817
2819
|
noShow(bookingId: string): Promise<Booking>;
|
|
2818
2820
|
freebusy(q: FreeBusyQuery): Promise<FreeBusyResponse>;
|
|
2821
|
+
message(input: MessageBookersInput): Promise<MessageBookersResult>;
|
|
2819
2822
|
slots: {
|
|
2820
2823
|
create(input: CreateSlotInput): Promise<BookingSlot>;
|
|
2821
2824
|
createMany(inputs: CreateSlotInput[], opts?: { seriesId?: string }): Promise<BookingSlot[]>;
|
|
@@ -5366,6 +5369,32 @@ interface FreeBusyResponse {
|
|
|
5366
5369
|
}
|
|
5367
5370
|
```
|
|
5368
5371
|
|
|
5372
|
+
**MessageBookersInput** (interface)
|
|
5373
|
+
```typescript
|
|
5374
|
+
interface MessageBookersInput {
|
|
5375
|
+
templateId: string
|
|
5376
|
+
channel?: 'email' | 'sms' | 'whatsapp' | 'push' | 'preferred'
|
|
5377
|
+
props?: Record<string, any>
|
|
5378
|
+
slotId?: string
|
|
5379
|
+
from?: string | Date
|
|
5380
|
+
to?: string | Date
|
|
5381
|
+
resourceType?: string
|
|
5382
|
+
resourceId?: string
|
|
5383
|
+
statuses?: BookingStatus[]
|
|
5384
|
+
ref?: string
|
|
5385
|
+
}
|
|
5386
|
+
```
|
|
5387
|
+
|
|
5388
|
+
**MessageBookersResult** (interface)
|
|
5389
|
+
```typescript
|
|
5390
|
+
interface MessageBookersResult {
|
|
5391
|
+
matched: number
|
|
5392
|
+
sent: number
|
|
5393
|
+
skipped: { noContact: number; duplicate: number; failed: number }
|
|
5394
|
+
failures: Array<{ bookingId: string; error: string }>
|
|
5395
|
+
}
|
|
5396
|
+
```
|
|
5397
|
+
|
|
5369
5398
|
**BookingAccessOptions** (interface)
|
|
5370
5399
|
```typescript
|
|
5371
5400
|
interface BookingAccessOptions {
|
|
@@ -7452,6 +7481,7 @@ interface IframeResponderOptions {
|
|
|
7452
7481
|
version?: string;
|
|
7453
7482
|
appUrl?: string;
|
|
7454
7483
|
initialPath?: string;
|
|
7484
|
+
params?: Record<string, string>;
|
|
7455
7485
|
isAdmin?: boolean;
|
|
7456
7486
|
cache?: CachedData;
|
|
7457
7487
|
onAuthLogin?: (
|
|
@@ -11902,6 +11932,9 @@ confirmed → no_show (admin).
|
|
|
11902
11932
|
**freebusy**(collectionId: string, appId: string, q: FreeBusyQuery, admin: boolean = false) → `Promise<FreeBusyResponse>`
|
|
11903
11933
|
Busy intervals per resource (bookings, blocks, slots' own blocks) — never booking details. Public only when the app sets `publicRead.bookings.freebusy: true`. GET /bookings/freebusy
|
|
11904
11934
|
|
|
11935
|
+
**message**(collectionId: string, appId: string, input: MessageBookersInput) → `Promise<MessageBookersResult>`
|
|
11936
|
+
Message the people booked (admin): a template to everyone on a slot, or everyone booked in a date range (optionally with one person / room), each send hydrated with their booking ({{ booking }}). No segment to make. POST /bookings/message
|
|
11937
|
+
|
|
11905
11938
|
### bookings.blocks
|
|
11906
11939
|
|
|
11907
11940
|
**create**(collectionId: string, appId: string, input: CreateBlockInput) → `Promise<BookingAllocation>`
|
|
@@ -326,11 +326,28 @@ Static deep-linkable states built into the app — fixed routes that exist regar
|
|
|
326
326
|
|
|
327
327
|
See the [Deep Link Discovery guide](deep-link-discovery.md) for the full dual-source pattern (static manifest routes + dynamic `appConfig.linkable`).
|
|
328
328
|
|
|
329
|
+
These are the app's **pages that a host opens**: portal menus and tabs, and websites that embed the app
|
|
330
|
+
(websites.md → "Using another app's pages"). A host opens them with its own signed-in session, so one
|
|
331
|
+
app can offer several pages that each know the visitor: book, upcoming bookings, past bookings, change a
|
|
332
|
+
booking.
|
|
333
|
+
|
|
334
|
+
```json
|
|
335
|
+
"linkable": [
|
|
336
|
+
{ "id": "book", "title": "Book an appointment", "path": "/",
|
|
337
|
+
"accepts": { "optional": ["service", "staff", "date"] } },
|
|
338
|
+
{ "id": "upcoming", "title": "Your upcoming bookings", "path": "/my-bookings" },
|
|
339
|
+
{ "id": "change", "title": "Change a booking", "path": "/booking",
|
|
340
|
+
"accepts": { "required": ["id"] } }
|
|
341
|
+
]
|
|
342
|
+
```
|
|
343
|
+
|
|
329
344
|
| Field | Type | Required | Description |
|
|
330
345
|
|-------|------|----------|-------------|
|
|
346
|
+
| `id` | string | ❌ | Stable id, unique within the app: hosts and sites refer to the page by it |
|
|
331
347
|
| `title` | string | ✅ | Human-readable label shown in menus and offered to AI agents |
|
|
332
348
|
| `path` | string | ❌ | Hash route within the app (defaults to `"/"` if omitted) |
|
|
333
|
-
| `params` | object | ❌ |
|
|
349
|
+
| `params` | object | ❌ | Params this entry PINS, appended to the URL — do **not** include platform params (`collectionId`, `productId`, etc.) |
|
|
350
|
+
| `accepts` | `{ required?: string[]; optional?: string[] }` | ❌ | The params a host may supply when it opens the page (e.g. which service to start on) |
|
|
334
351
|
|
|
335
352
|
#### `publicViews`
|
|
336
353
|
|
|
@@ -371,7 +388,7 @@ An app can have any combination of three setup styles. Only public views are ext
|
|
|
371
388
|
| Style | Declared by | Lives | Context |
|
|
372
389
|
|-------|-------------|-------|---------|
|
|
373
390
|
| **Portal components** — where the app renders inside portal (collection / product / batch / variant / proof) | module registry `components.*` (set at publish) | Inside portal | Physical twin (QR/NFC scan) |
|
|
374
|
-
| **
|
|
391
|
+
| **App pages** — the entry points portal menus/tabs link to and websites embed (e.g. list vs calendar; book vs my bookings) | `linkable` / `DeepLinkEntry` | Inside a host: portal or a website | The host: its signed-in session, plus the page's `accepts` params |
|
|
375
392
|
| **Public views** — standalone external screens | `publicViews` (this block) | Outside portal | URL params only |
|
|
376
393
|
|
|
377
394
|
#### `records`
|
package/dist/docs/bookings.md
CHANGED
|
@@ -324,8 +324,80 @@ Recorded after each write commits, for flows and app functions (`trigger: { type
|
|
|
324
324
|
`booking.no_show`, `booking.hold_expired`, `slot.full`, `slot.place_freed` (for waitlist promotion).
|
|
325
325
|
Context: `{ appId, status, slotIds }` (bookings) / `{ appId, remaining }` (slots).
|
|
326
326
|
|
|
327
|
-
Reminders before a booking (`booking.starts_in`) are coming; until then,
|
|
328
|
-
|
|
327
|
+
Reminders before a booking (`booking.starts_in`) are coming; until then, send a reminder template to
|
|
328
|
+
tomorrow's bookers with `bookings.message` (below) from a cron function.
|
|
329
|
+
|
|
330
|
+
---
|
|
331
|
+
|
|
332
|
+
## Confirmations and messages
|
|
333
|
+
|
|
334
|
+
Nothing is emailed by default: your app decides what goes out, in **flows on booking events**, with
|
|
335
|
+
**templates the business can reword in Studio** (Comms → Templates). The platform does the sending,
|
|
336
|
+
the hydration and the consent.
|
|
337
|
+
|
|
338
|
+
### Flows on booking events
|
|
339
|
+
|
|
340
|
+
A site, or an app on behalf of a business, sets them up through the cloud blueprint
|
|
341
|
+
(`cloud/flows.json` in a site; `POST /admin/collection/:cId/blueprint/apply` with
|
|
342
|
+
`{ appId, blueprint: { templates, flows } }` from an app's admin screen). Additive: re-applying never
|
|
343
|
+
removes what is there, and a template edited in Studio keeps its edits unless the blueprint sends new
|
|
344
|
+
wording.
|
|
345
|
+
|
|
346
|
+
```json
|
|
347
|
+
{
|
|
348
|
+
"templates": {
|
|
349
|
+
"booking-confirmed": { "name": "You're booked", "email": { "subject": "{{ booking.serviceName }} on {{ booking.when.date }}", "body": "<p>Hi {{ contact.firstName }}, see you at {{ booking.when.time }}.</p>" }, "sms": { "body": "Booked: {{ booking.when.shortDate }} {{ booking.when.time }}" } },
|
|
350
|
+
"booking-requested": { "name": "Request received", "email": { "subject": "We got your request", "body": "<p>We'll confirm shortly.</p>" } },
|
|
351
|
+
"staff-new-booking": { "name": "You've been booked", "email": { "subject": "New appointment", "body": "<p>{{ booking.customer.name }}, {{ booking.when.dateTime }}</p>" } }
|
|
352
|
+
},
|
|
353
|
+
"flows": [
|
|
354
|
+
{ "when": { "event": "booking.confirmed" }, "do": [{ "send": "booking-confirmed", "to": "contact" }, { "send": "staff-new-booking", "to": "resource" }, { "send": "team-new-booking", "to": "team" }] },
|
|
355
|
+
{ "when": { "event": "booking.created", "status": "pending" }, "do": [{ "send": "booking-requested", "to": "contact" }] }
|
|
356
|
+
]
|
|
357
|
+
}
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
- `when.event`: `booking.created` / `confirmed` / `cancelled` / `moved` / `completed` / `no_show` /
|
|
361
|
+
`hold_expired`, `slot.full` / `slot.place_freed`; `when.status` narrows to the booking's status at the
|
|
362
|
+
time (`"pending"` for "request received").
|
|
363
|
+
- `to`: `contact` — the booker, by their preferred channel (`channel` to force one; a template needs an
|
|
364
|
+
`sms` part to go by text); `team` — the collection's team addresses; `resource` — the booked
|
|
365
|
+
person or room: a resource that is a contact, or a record with `data.email` / `data.contactId`.
|
|
366
|
+
A **public** resource record (a staff profile the booking page lists) keeps its address on a
|
|
367
|
+
**private child record** instead (`visibility: 'admin'`, `parentType: 'record'`, `parentId` = the
|
|
368
|
+
resource) — the platform looks there too, and the address never ships with the public listing.
|
|
369
|
+
- Who gets what, in templates: `{{ booking }}` is the booking **ready-formatted**: its fields and
|
|
370
|
+
`data`, plus `serviceName`, `staffName`, `customer`, `durationMinutes`, `when` (`date`,
|
|
371
|
+
`shortDate`, `time`, `endTime`, `dateTime`, `timezone` — in the business's timezone) and, when the
|
|
372
|
+
recipient's timezone is known and differs, `whenFor` (the same in theirs); `editToken` only when the
|
|
373
|
+
recipient is the booker (a guest's manage link). `{{ contact }}` is the recipient; `{{ appConfig }}`
|
|
374
|
+
the app's public config (a booking page's address, the business name) for sends to the contact.
|
|
375
|
+
|
|
376
|
+
### Who is reachable
|
|
377
|
+
|
|
378
|
+
A **guest** who books with an email or phone gets a **contact** (found by that identity or made,
|
|
379
|
+
filling only empty fields: a form's details never overwrite a known contact), and so does a customer
|
|
380
|
+
**staff book in by hand** (`customer: { name, email, phone }` on the body or in `data`). Admin
|
|
381
|
+
creates may pass `contactId` instead. A booking with no contact gets no messages: show the booker why
|
|
382
|
+
an email is worth giving.
|
|
383
|
+
|
|
384
|
+
### Messaging everyone booked — `bookings.message`
|
|
385
|
+
|
|
386
|
+
No list to build, no segment to save: send a template to everyone on a slot, or everyone booked in a
|
|
387
|
+
window (optionally with one resource), each hydrated with **their own** booking.
|
|
388
|
+
|
|
389
|
+
```ts
|
|
390
|
+
// Everyone on tomorrow's 10:00 class
|
|
391
|
+
await SL.bookings.message(collectionId, appId, { templateId, slotId })
|
|
392
|
+
// Everyone booked with Sam next week, by email even if they prefer texts
|
|
393
|
+
await SL.bookings.message(collectionId, appId, { templateId, channel: 'email', from, to, resourceType: 'record', resourceId: samId })
|
|
394
|
+
// → { matched, sent, skipped: { noContact, duplicate, failed }, failures: [{ bookingId, reason }] }
|
|
395
|
+
```
|
|
396
|
+
|
|
397
|
+
Statuses default to held, pending and confirmed; at most 500 recipients per call (narrow the window
|
|
398
|
+
for more). One message per contact on a slot, one per booking in a window. Also
|
|
399
|
+
`ctx.sl.bookings.message(input)` in a server function (`sl:bookings:write`) — a cron function that
|
|
400
|
+
messages tomorrow's bookers is a reminder system in five lines.
|
|
329
401
|
|
|
330
402
|
---
|
|
331
403
|
|
|
@@ -343,6 +415,7 @@ All take `(collectionId, appId, …)`; `admin` is the last parameter where both
|
|
|
343
415
|
| `bookings.confirm / cancel / move(…, admin?, { editToken }?)` | ✓ | per `ownerActions.bookings` |
|
|
344
416
|
| `bookings.extendHold / complete / noShow` | ✓ | — |
|
|
345
417
|
| `bookings.freebusy(q, admin?)` | ✓ | if `publicRead.bookings.freebusy` |
|
|
418
|
+
| `bookings.message(input)` | ✓ | — |
|
|
346
419
|
| `bookings.slots.create / createMany / update / remove` | ✓ | — |
|
|
347
420
|
| `bookings.slots.list / get(…, admin?)` | ✓ | open, public slots |
|
|
348
421
|
| `bookings.blocks.create / list / remove` | ✓ | — |
|
|
@@ -172,6 +172,9 @@ Both sources use the same entry shape:
|
|
|
172
172
|
|
|
173
173
|
```typescript
|
|
174
174
|
interface DeepLinkEntry {
|
|
175
|
+
/** Stable id, unique within the app (optional): hosts and websites refer to the page by it */
|
|
176
|
+
id?: string;
|
|
177
|
+
|
|
175
178
|
/** Human-readable label for this state (required) */
|
|
176
179
|
title: string;
|
|
177
180
|
|
|
@@ -193,6 +196,13 @@ interface DeepLinkEntry {
|
|
|
193
196
|
*/
|
|
194
197
|
params?: Record<string, string>;
|
|
195
198
|
|
|
199
|
+
/**
|
|
200
|
+
* The params a host may supply when it opens this page (optional), e.g. a booking page that can
|
|
201
|
+
* start on one service: `{ optional: ['service', 'staff', 'date'] }`. `params` are pinned by the
|
|
202
|
+
* entry; `accepts` are filled in by whoever opens it.
|
|
203
|
+
*/
|
|
204
|
+
accepts?: { required?: string[]; optional?: string[] };
|
|
205
|
+
|
|
196
206
|
/**
|
|
197
207
|
* When `true`, this entry is also available as a dynamic data context for widgets.
|
|
198
208
|
*
|
|
@@ -211,7 +221,9 @@ interface DeepLinkEntry {
|
|
|
211
221
|
|-------|----------|-------|
|
|
212
222
|
| `title` | ✅ | Displayed in menus and offered to AI agents. Should be concise and human-readable. |
|
|
213
223
|
| `path` | ❌ | Hash route within the app. Defaults to `/` if omitted. |
|
|
224
|
+
| `id` | ❌ | Stable id for hosts and websites to refer to the page by. |
|
|
214
225
|
| `params` | ❌ | App-specific query params only. Platform params are injected automatically. |
|
|
226
|
+
| `accepts` | ❌ | The params whoever opens the page may supply (`required` / `optional`). |
|
|
215
227
|
| `widget` | ❌ | Set to `true` to make this entry selectable in the widget config picker as a dynamic data context. |
|
|
216
228
|
|
|
217
229
|
> **Tip:** Different entries can share the same `path` if they differ by `params` — for example, two entries pointing at `/settings` with different `tab` params.
|
|
@@ -103,6 +103,7 @@ interface IframeResponderOptions {
|
|
|
103
103
|
version?: 'stable' | 'development'; // App version (default: 'stable')
|
|
104
104
|
appUrl?: string; // Override URL (for local dev)
|
|
105
105
|
initialPath?: string; // Initial hash path (e.g., '/settings')
|
|
106
|
+
params?: Record<string, string>; // The page's own params (e.g. { service: 'svc_123' }); context params win
|
|
106
107
|
|
|
107
108
|
// Data
|
|
108
109
|
cache?: CachedData; // Pre-cached data
|
|
@@ -517,7 +517,7 @@ Prefer the platform primitives above over a dependency; when you do need one, pi
|
|
|
517
517
|
| Check a tapped tag is real | `ctx.sl.tags.resolve(proofId)` → `{ id, claimSetId, code }` or `null` | `sl:tags:read` |
|
|
518
518
|
| Put a visitor's details on a contact | `ctx.sl.contacts.upsert({ email, firstName, … })` → `{ contactId, created }` (fills empty fields only) | `sl:contacts:write` |
|
|
519
519
|
| Record "they did X" | `ctx.sl.interactions.record({ interactionId, contactId, outcome, metadata })` | `sl:interactions:write` |
|
|
520
|
-
| Take a booking with your own checks (hours, queue token, payment) | `ctx.sl.bookings.create({ slotId \| resource + time, contactId, status, idempotencyKey })` — never oversold, never double-booked; also `confirm` / `cancel` / `move` / `mine` / `freebusy` / `slots.*` / `blocks.*` (see [bookings.md](bookings.md)) | `sl:bookings:write` (`sl:bookings:read` to read) |
|
|
520
|
+
| Take a booking with your own checks (hours, queue token, payment) | `ctx.sl.bookings.create({ slotId \| resource + time, contactId, status, idempotencyKey })` — never oversold, never double-booked; also `confirm` / `cancel` / `move` / `mine` / `freebusy` / `message` (everyone booked, hydrated) / `slots.*` / `blocks.*` (see [bookings.md](bookings.md)) | `sl:bookings:write` (`sl:bookings:read` to read) |
|
|
521
521
|
|
|
522
522
|
Notes:
|
|
523
523
|
- **Talking to the SmartLinks core is `ctx.sl`, not HTTP.** The common pattern — *validate /
|
|
@@ -636,6 +636,18 @@ try {
|
|
|
636
636
|
}
|
|
637
637
|
```
|
|
638
638
|
|
|
639
|
+
`ctx.sl.bookings.message({ templateId, slotId | from, to, resourceType?, resourceId?, channel? })` sends a
|
|
640
|
+
template to everyone booked (each hydrated with their own booking) — the five-line reminder cron:
|
|
641
|
+
|
|
642
|
+
```ts
|
|
643
|
+
// manifest: { name: 'remind', trigger: { type: 'cron', schedule: '0 17 * * *' }, capabilities: ['sl:bookings:write'] }
|
|
644
|
+
export async function remind(ctx) {
|
|
645
|
+
const from = new Date(); from.setDate(from.getDate() + 1); from.setHours(0, 0, 0, 0)
|
|
646
|
+
const to = new Date(from); to.setDate(to.getDate() + 1)
|
|
647
|
+
return ctx.sl.bookings.message({ templateId: 'bp.' + ctx.collectionId + '.reminder', from, to })
|
|
648
|
+
}
|
|
649
|
+
```
|
|
650
|
+
|
|
639
651
|
The local harness runs bookings in memory with the same rules (capacity, per-person limits, no
|
|
640
652
|
overlaps, idempotency, holds): `ctx.memory.bookings`.
|
|
641
653
|
|
package/dist/docs/websites.md
CHANGED
|
@@ -66,6 +66,32 @@ bytes. `_server/` is never served to visitors.
|
|
|
66
66
|
doing work that belongs in a server function or a scheduled job.
|
|
67
67
|
- **No secrets** in the server bundle and no Node built-ins beyond `nodejs_compat`.
|
|
68
68
|
|
|
69
|
+
## Using another app's pages
|
|
70
|
+
|
|
71
|
+
Some apps do a job a site shouldn't rebuild: a booking journey holds the time rules, the clash
|
|
72
|
+
handling and the confirmation; "my bookings" holds who may change what. Those apps declare their pages
|
|
73
|
+
in `linkable` (app-manifest.md), each with a stable `id`, a `path` and the params it `accepts`. A site
|
|
74
|
+
embeds one with **`IframeResponder`** (iframe-responder.md), from the browser:
|
|
75
|
+
|
|
76
|
+
```typescript
|
|
77
|
+
const site = SL.site.current()
|
|
78
|
+
const responder = new SL.IframeResponder({
|
|
79
|
+
collectionId: site.collectionId,
|
|
80
|
+
appId: 'appointments', // the app's id on this collection
|
|
81
|
+
initialPath: '/my-bookings', // the page's `path`
|
|
82
|
+
params: { service: 'svc_123' }, // what the page `accepts`
|
|
83
|
+
onResize: (h) => { iframe.style.height = `${h}px` },
|
|
84
|
+
})
|
|
85
|
+
iframe.src = await responder.attach(iframe)
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
- **The visitor's session goes with it.** The page's API calls are proxied through the site, so it
|
|
89
|
+
acts as whoever is signed in to the site (or as a guest). Never pass a token in the URL.
|
|
90
|
+
- **The app must be installed** on the site's collection; its address comes from there.
|
|
91
|
+
- **Show the app's content natively where you can** (its `headless` data: services, staff, events)
|
|
92
|
+
for your own design and SEO, and embed the page only for the part that does the work.
|
|
93
|
+
- Pages render on the client, after load: they aren't part of the server-rendered HTML.
|
|
94
|
+
|
|
69
95
|
## Caching
|
|
70
96
|
|
|
71
97
|
The router keeps a rendered page at the edge when your response says it may:
|
package/dist/iframeResponder.js
CHANGED
|
@@ -155,11 +155,16 @@ export class IframeResponder {
|
|
|
155
155
|
// URL Building
|
|
156
156
|
// ===========================================================================
|
|
157
157
|
buildIframeSrc() {
|
|
158
|
-
var _a, _b;
|
|
158
|
+
var _a, _b, _c;
|
|
159
159
|
if (!this.appUrl) {
|
|
160
160
|
throw new Error('App URL not resolved');
|
|
161
161
|
}
|
|
162
162
|
const params = new URLSearchParams();
|
|
163
|
+
// The page's own params first: the platform's context below always wins.
|
|
164
|
+
for (const [key, value] of Object.entries((_a = this.options.params) !== null && _a !== void 0 ? _a : {})) {
|
|
165
|
+
if (value != null && value !== '')
|
|
166
|
+
params.set(key, String(value));
|
|
167
|
+
}
|
|
163
168
|
// Required context
|
|
164
169
|
params.set('collectionId', this.options.collectionId);
|
|
165
170
|
params.set('appId', this.options.appId);
|
|
@@ -171,13 +176,13 @@ export class IframeResponder {
|
|
|
171
176
|
if (this.options.isAdmin)
|
|
172
177
|
params.set('isAdmin', 'true');
|
|
173
178
|
// Dark mode from collection
|
|
174
|
-
const isDark = (
|
|
179
|
+
const isDark = (_c = (_b = this.cache.collection) === null || _b === void 0 ? void 0 : _b.dark) !== null && _c !== void 0 ? _c : false;
|
|
175
180
|
params.set('dark', isDark ? '1' : '0');
|
|
176
181
|
// Parent URL for redirects
|
|
177
182
|
try {
|
|
178
183
|
params.set('parentUrl', window.location.href);
|
|
179
184
|
}
|
|
180
|
-
catch (
|
|
185
|
+
catch (_d) {
|
|
181
186
|
// Ignore if can't access location
|
|
182
187
|
}
|
|
183
188
|
// Encode theme from collection
|
|
@@ -192,7 +197,7 @@ export class IframeResponder {
|
|
|
192
197
|
params.set('theme', btoa(JSON.stringify(themeData)));
|
|
193
198
|
}
|
|
194
199
|
}
|
|
195
|
-
catch (
|
|
200
|
+
catch (_e) {
|
|
196
201
|
// Ignore encoding errors
|
|
197
202
|
}
|
|
198
203
|
}
|
|
@@ -210,7 +215,8 @@ export class IframeResponder {
|
|
|
210
215
|
const path = this.options.initialPath.startsWith('/') ? this.options.initialPath : '/' + this.options.initialPath;
|
|
211
216
|
// Insert path before the ?
|
|
212
217
|
const [beforeHash, afterHash] = baseWithHash.split('#');
|
|
213
|
-
|
|
218
|
+
// App addresses end in "#/": join without doubling the slash ("#//my-bookings" matches no route).
|
|
219
|
+
finalUrl = `${beforeHash}#${(afterHash || '').replace(/\/+$/, '')}${path}?${existingParams.toString()}`;
|
|
214
220
|
}
|
|
215
221
|
else {
|
|
216
222
|
finalUrl = `${baseWithHash}?${existingParams.toString()}`;
|
|
@@ -765,8 +771,12 @@ export function isAdminFromRoles(user, collection, proof) {
|
|
|
765
771
|
* Standalone helper for cases where you don't need the full IframeResponder.
|
|
766
772
|
*/
|
|
767
773
|
export function buildIframeSrc(options) {
|
|
768
|
-
var _a, _b;
|
|
774
|
+
var _a, _b, _c;
|
|
769
775
|
const params = new URLSearchParams();
|
|
776
|
+
for (const [key, value] of Object.entries((_a = options.params) !== null && _a !== void 0 ? _a : {})) {
|
|
777
|
+
if (value != null && value !== '')
|
|
778
|
+
params.set(key, String(value));
|
|
779
|
+
}
|
|
770
780
|
params.set('collectionId', options.collectionId);
|
|
771
781
|
params.set('appId', options.appId);
|
|
772
782
|
if (options.productId)
|
|
@@ -776,14 +786,14 @@ export function buildIframeSrc(options) {
|
|
|
776
786
|
if (options.isAdmin)
|
|
777
787
|
params.set('isAdmin', 'true');
|
|
778
788
|
params.set('dark', options.dark ? '1' : '0');
|
|
779
|
-
if (((
|
|
789
|
+
if (((_b = options.theme) === null || _b === void 0 ? void 0 : _b.primary) || ((_c = options.theme) === null || _c === void 0 ? void 0 : _c.secondary)) {
|
|
780
790
|
const themeData = { p: options.theme.primary, s: options.theme.secondary };
|
|
781
791
|
params.set('theme', btoa(JSON.stringify(themeData)));
|
|
782
792
|
}
|
|
783
793
|
try {
|
|
784
794
|
params.set('parentUrl', window.location.href);
|
|
785
795
|
}
|
|
786
|
-
catch (
|
|
796
|
+
catch (_d) {
|
|
787
797
|
// Ignore
|
|
788
798
|
}
|
|
789
799
|
let base = options.appUrl.replace(/#\/?$/, '').replace(/\/$/, '');
|
package/dist/openapi.yaml
CHANGED
|
@@ -10291,6 +10291,32 @@ paths:
|
|
|
10291
10291
|
description: Unauthorized
|
|
10292
10292
|
404:
|
|
10293
10293
|
description: Not found
|
|
10294
|
+
/message:
|
|
10295
|
+
post:
|
|
10296
|
+
tags:
|
|
10297
|
+
- bookings
|
|
10298
|
+
summary: "Message the people booked (admin): a template to everyone on a slot, or everyone booked in a date range (optionally with one person / room), each send hydrated with their booking ({{ booking }})."
|
|
10299
|
+
operationId: bookings_message
|
|
10300
|
+
security: []
|
|
10301
|
+
responses:
|
|
10302
|
+
200:
|
|
10303
|
+
description: Success
|
|
10304
|
+
content:
|
|
10305
|
+
application/json:
|
|
10306
|
+
schema:
|
|
10307
|
+
$ref: "#/components/schemas/MessageBookersResult"
|
|
10308
|
+
400:
|
|
10309
|
+
description: Bad request
|
|
10310
|
+
401:
|
|
10311
|
+
description: Unauthorized
|
|
10312
|
+
404:
|
|
10313
|
+
description: Not found
|
|
10314
|
+
requestBody:
|
|
10315
|
+
required: true
|
|
10316
|
+
content:
|
|
10317
|
+
application/json:
|
|
10318
|
+
schema:
|
|
10319
|
+
$ref: "#/components/schemas/MessageBookersInput"
|
|
10294
10320
|
/platform/location:
|
|
10295
10321
|
post:
|
|
10296
10322
|
tags:
|
|
@@ -19883,6 +19909,8 @@ components:
|
|
|
19883
19909
|
DeepLinkEntry:
|
|
19884
19910
|
type: object
|
|
19885
19911
|
properties:
|
|
19912
|
+
id:
|
|
19913
|
+
type: string
|
|
19886
19914
|
title:
|
|
19887
19915
|
type: string
|
|
19888
19916
|
path:
|
|
@@ -19891,6 +19919,8 @@ components:
|
|
|
19891
19919
|
type: object
|
|
19892
19920
|
additionalProperties:
|
|
19893
19921
|
type: string
|
|
19922
|
+
accepts:
|
|
19923
|
+
$ref: "#/components/schemas/PublicViewParams"
|
|
19894
19924
|
widget:
|
|
19895
19925
|
type: object
|
|
19896
19926
|
additionalProperties: true
|
|
@@ -24706,6 +24736,62 @@ components:
|
|
|
24706
24736
|
- from
|
|
24707
24737
|
- to
|
|
24708
24738
|
- resources
|
|
24739
|
+
MessageBookersInput:
|
|
24740
|
+
type: object
|
|
24741
|
+
properties:
|
|
24742
|
+
templateId:
|
|
24743
|
+
type: string
|
|
24744
|
+
channel:
|
|
24745
|
+
type: string
|
|
24746
|
+
enum:
|
|
24747
|
+
- email
|
|
24748
|
+
- sms
|
|
24749
|
+
- whatsapp
|
|
24750
|
+
- push
|
|
24751
|
+
- preferred
|
|
24752
|
+
props:
|
|
24753
|
+
type: object
|
|
24754
|
+
additionalProperties: true
|
|
24755
|
+
slotId:
|
|
24756
|
+
type: string
|
|
24757
|
+
from:
|
|
24758
|
+
type: object
|
|
24759
|
+
additionalProperties: true
|
|
24760
|
+
to:
|
|
24761
|
+
type: object
|
|
24762
|
+
additionalProperties: true
|
|
24763
|
+
resourceType:
|
|
24764
|
+
type: string
|
|
24765
|
+
resourceId:
|
|
24766
|
+
type: string
|
|
24767
|
+
statuses:
|
|
24768
|
+
type: array
|
|
24769
|
+
items:
|
|
24770
|
+
$ref: "#/components/schemas/BookingStatus"
|
|
24771
|
+
ref:
|
|
24772
|
+
type: string
|
|
24773
|
+
required:
|
|
24774
|
+
- templateId
|
|
24775
|
+
MessageBookersResult:
|
|
24776
|
+
type: object
|
|
24777
|
+
properties:
|
|
24778
|
+
matched:
|
|
24779
|
+
type: number
|
|
24780
|
+
sent:
|
|
24781
|
+
type: number
|
|
24782
|
+
skipped:
|
|
24783
|
+
type: object
|
|
24784
|
+
additionalProperties: true
|
|
24785
|
+
failures:
|
|
24786
|
+
type: array
|
|
24787
|
+
items:
|
|
24788
|
+
type: object
|
|
24789
|
+
additionalProperties: true
|
|
24790
|
+
required:
|
|
24791
|
+
- matched
|
|
24792
|
+
- sent
|
|
24793
|
+
- skipped
|
|
24794
|
+
- failures
|
|
24709
24795
|
BookingAccessOptions:
|
|
24710
24796
|
type: object
|
|
24711
24797
|
properties:
|
|
@@ -27570,6 +27656,10 @@ components:
|
|
|
27570
27656
|
type: string
|
|
27571
27657
|
initialPath:
|
|
27572
27658
|
type: string
|
|
27659
|
+
params:
|
|
27660
|
+
type: object
|
|
27661
|
+
additionalProperties:
|
|
27662
|
+
type: string
|
|
27573
27663
|
isAdmin:
|
|
27574
27664
|
type: boolean
|
|
27575
27665
|
cache:
|
|
@@ -32,6 +32,17 @@ export declare function bookingsMemoryImpl(mem: BookingsMemory, caller?: {
|
|
|
32
32
|
cancel(bookingId: string, opts?: any): Promise<any>;
|
|
33
33
|
move(bookingId: string, input?: any): Promise<any>;
|
|
34
34
|
extendHold(bookingId: string, holdMinutes: number): Promise<any>;
|
|
35
|
+
/** Counts who would be messaged (no sends in memory); inject `sl.bookings.message` to assert sends. */
|
|
36
|
+
message(input?: any): Promise<{
|
|
37
|
+
matched: number;
|
|
38
|
+
sent: number;
|
|
39
|
+
skipped: {
|
|
40
|
+
noContact: number;
|
|
41
|
+
duplicate: number;
|
|
42
|
+
failed: number;
|
|
43
|
+
};
|
|
44
|
+
failures: any[];
|
|
45
|
+
}>;
|
|
35
46
|
complete(bookingId: string): Promise<any>;
|
|
36
47
|
noShow(bookingId: string): Promise<any>;
|
|
37
48
|
freebusy(q?: any): Promise<{
|
|
@@ -280,6 +280,28 @@ export function bookingsMemoryImpl(mem, caller = {}) {
|
|
|
280
280
|
b.holdExpiresAt = iso(Math.min(now() + (holdMinutes !== null && holdMinutes !== void 0 ? holdMinutes : 15) * 60000, ms(b.createdAt) + 24 * 3600000));
|
|
281
281
|
return copy(withAllocations(b));
|
|
282
282
|
},
|
|
283
|
+
/** Counts who would be messaged (no sends in memory); inject `sl.bookings.message` to assert sends. */
|
|
284
|
+
async message(input = {}) {
|
|
285
|
+
const list = input.slotId
|
|
286
|
+
? (await this.query({ slotId: input.slotId, status: (input.statuses || ['held', 'pending', 'confirmed']).join(',') })).data
|
|
287
|
+
: (await this.query({ from: input.from, to: input.to, status: (input.statuses || ['held', 'pending', 'confirmed']).join(',') })).data;
|
|
288
|
+
const seen = new Set();
|
|
289
|
+
const out = { matched: list.length, sent: 0, skipped: { noContact: 0, duplicate: 0, failed: 0 }, failures: [] };
|
|
290
|
+
for (const b of list) {
|
|
291
|
+
if (!b.contactId) {
|
|
292
|
+
out.skipped.noContact++;
|
|
293
|
+
continue;
|
|
294
|
+
}
|
|
295
|
+
const key = input.slotId ? b.contactId : `${b.contactId}:${b.id}`;
|
|
296
|
+
if (seen.has(key)) {
|
|
297
|
+
out.skipped.duplicate++;
|
|
298
|
+
continue;
|
|
299
|
+
}
|
|
300
|
+
seen.add(key);
|
|
301
|
+
out.sent++;
|
|
302
|
+
}
|
|
303
|
+
return out;
|
|
304
|
+
},
|
|
283
305
|
async complete(bookingId) { return outcome(find(bookingId), 'completed'); },
|
|
284
306
|
async noShow(bookingId) { return outcome(find(bookingId), 'no_show'); },
|
|
285
307
|
async freebusy(q = {}) {
|
package/dist/testing/index.js
CHANGED
|
@@ -274,7 +274,7 @@ export function createFunctionTestContext(opts) {
|
|
|
274
274
|
const bkFn = (obj, name) => (obj && typeof obj[name] === 'function' ? obj[name].bind(obj) : undefined);
|
|
275
275
|
const BOOKING_READS = new Set(['get', 'query', 'mine', 'freebusy']);
|
|
276
276
|
sl.bookings = {};
|
|
277
|
-
for (const m of ['create', 'get', 'query', 'mine', 'update', 'confirm', 'cancel', 'move', 'extendHold', 'complete', 'noShow', 'freebusy']) {
|
|
277
|
+
for (const m of ['create', 'get', 'query', 'mine', 'update', 'confirm', 'cancel', 'move', 'extendHold', 'complete', 'noShow', 'freebusy', 'message']) {
|
|
278
278
|
sl.bookings[m] = gated('bookings', BOOKING_READS.has(m) ? 'read' : 'write', bkFn(bk, m), `bookings.${m}`);
|
|
279
279
|
}
|
|
280
280
|
sl.bookings.slots = {};
|
|
@@ -4,7 +4,7 @@ import type { AppDataDeclaration, AppHeadlessDeclaration } from './headless.js';
|
|
|
4
4
|
import type { RealtimeChannelDeclaration } from './realtime.js';
|
|
5
5
|
import type { AppManifestAdminCards } from './adminCards.js';
|
|
6
6
|
import type { PaginatedResponse } from './appObjects.js';
|
|
7
|
-
import type { Booking, BookingSlot, BookingAllocation, CreateBookingInput, UpdateBookingInput, MoveBookingInput, BookingListQueryParams, CreateSlotInput, UpdateSlotInput, SlotListQueryParams, CreateBlockInput, BlockListQueryParams, FreeBusyQuery, FreeBusyResponse } from './bookings.js';
|
|
7
|
+
import type { Booking, BookingSlot, BookingAllocation, CreateBookingInput, UpdateBookingInput, MoveBookingInput, BookingListQueryParams, CreateSlotInput, UpdateSlotInput, SlotListQueryParams, CreateBlockInput, BlockListQueryParams, FreeBusyQuery, FreeBusyResponse, MessageBookersInput, MessageBookersResult } from './bookings.js';
|
|
8
8
|
/**
|
|
9
9
|
* A bundle (widget or container) as returned by the collection widgets endpoint.
|
|
10
10
|
*
|
|
@@ -109,6 +109,12 @@ export interface AppContainerComponent {
|
|
|
109
109
|
* { "title": "About Us", "params": { "pageId": "about-us" } }
|
|
110
110
|
*/
|
|
111
111
|
export interface DeepLinkEntry {
|
|
112
|
+
/**
|
|
113
|
+
* Stable id for this page, unique within the app (optional). Hosts and websites refer to the page
|
|
114
|
+
* by it (e.g. a site embeds `{ app: 'appointments', page: 'book' }`), so a renamed `title` or a moved
|
|
115
|
+
* `path` doesn't break them.
|
|
116
|
+
*/
|
|
117
|
+
id?: string;
|
|
112
118
|
/** Human-readable label shown in menus and offered to AI agents */
|
|
113
119
|
title: string;
|
|
114
120
|
/**
|
|
@@ -123,6 +129,12 @@ export interface DeepLinkEntry {
|
|
|
123
129
|
* those are injected by the platform automatically.
|
|
124
130
|
*/
|
|
125
131
|
params?: Record<string, string>;
|
|
132
|
+
/**
|
|
133
|
+
* The params a host may supply when it opens this page (in addition to the platform's own), e.g. a
|
|
134
|
+
* booking page that can start on one service: `{ optional: ['service', 'staff', 'date'] }`. `params`
|
|
135
|
+
* above are the values this entry PINS; `accepts` are the ones the caller fills in.
|
|
136
|
+
*/
|
|
137
|
+
accepts?: PublicViewParams;
|
|
126
138
|
/**
|
|
127
139
|
* When `true`, this entry is also available as a dynamic data context for widgets
|
|
128
140
|
* (in addition to being a navigable page / container route).
|
|
@@ -449,6 +461,8 @@ export interface ServerFunctionBookings {
|
|
|
449
461
|
noShow(bookingId: string): Promise<Booking>;
|
|
450
462
|
/** Busy intervals per resource — feed them to `utils.freeTimes` with your schedule. */
|
|
451
463
|
freebusy(q: FreeBusyQuery): Promise<FreeBusyResponse>;
|
|
464
|
+
/** A template to everyone booked on a slot / in a range, each hydrated with their booking. */
|
|
465
|
+
message(input: MessageBookersInput): Promise<MessageBookersResult>;
|
|
452
466
|
slots: {
|
|
453
467
|
create(input: CreateSlotInput): Promise<BookingSlot>;
|
|
454
468
|
createMany(inputs: CreateSlotInput[], opts?: {
|
|
@@ -854,10 +868,10 @@ export interface AppManifest {
|
|
|
854
868
|
components: AppContainerComponent[];
|
|
855
869
|
};
|
|
856
870
|
/**
|
|
857
|
-
*
|
|
858
|
-
*
|
|
859
|
-
*
|
|
860
|
-
*
|
|
871
|
+
* The app's PAGES that a host opens — portal menus (bottom menu, side menus, tabs) and WEBSITES that
|
|
872
|
+
* embed the app (IframeResponder). Use these when one app has several ways in (a list view and a
|
|
873
|
+
* calendar view; book, upcoming bookings, change a booking). A host opens them with its own signed-in
|
|
874
|
+
* session: the page's API calls go through the host, so it acts as the person signed in there.
|
|
861
875
|
* Fixed routes are declared here at build time; dynamic content entries (e.g. CMS pages) are stored
|
|
862
876
|
* separately in `appConfig.linkable` — merge both for the full navigable set.
|
|
863
877
|
* @see DeepLinkEntry
|
package/dist/types/bookings.d.ts
CHANGED
|
@@ -266,6 +266,37 @@ export interface FreeBusyResponse {
|
|
|
266
266
|
busy: FreeBusyInterval[];
|
|
267
267
|
}>;
|
|
268
268
|
}
|
|
269
|
+
/** `bookings.message`: a template to everyone booked on a slot, or in a date range. */
|
|
270
|
+
export interface MessageBookersInput {
|
|
271
|
+
/** A comms template of the collection (edited in Studio). */
|
|
272
|
+
templateId: string;
|
|
273
|
+
channel?: 'email' | 'sms' | 'whatsapp' | 'push' | 'preferred';
|
|
274
|
+
/** Extra Liquid variables, on top of {{ booking }}, {{ contact }} and {{ appConfig }}. */
|
|
275
|
+
props?: Record<string, any>;
|
|
276
|
+
/** Everyone on this slot … */
|
|
277
|
+
slotId?: string;
|
|
278
|
+
/** … or everyone booked in [from, to), optionally with one resource. */
|
|
279
|
+
from?: string | Date;
|
|
280
|
+
to?: string | Date;
|
|
281
|
+
resourceType?: string;
|
|
282
|
+
resourceId?: string;
|
|
283
|
+
/** Default: held, pending, confirmed. */
|
|
284
|
+
statuses?: BookingStatus[];
|
|
285
|
+
ref?: string;
|
|
286
|
+
}
|
|
287
|
+
export interface MessageBookersResult {
|
|
288
|
+
matched: number;
|
|
289
|
+
sent: number;
|
|
290
|
+
skipped: {
|
|
291
|
+
noContact: number;
|
|
292
|
+
duplicate: number;
|
|
293
|
+
failed: number;
|
|
294
|
+
};
|
|
295
|
+
failures: Array<{
|
|
296
|
+
bookingId: string;
|
|
297
|
+
error: string;
|
|
298
|
+
}>;
|
|
299
|
+
}
|
|
269
300
|
/** Public calls on a booking made by an anonymous guest: pass the edit token returned at create. */
|
|
270
301
|
export interface BookingAccessOptions {
|
|
271
302
|
editToken?: string;
|
|
@@ -24,6 +24,12 @@ export interface IframeResponderOptions {
|
|
|
24
24
|
appUrl?: string;
|
|
25
25
|
/** Initial hash path (e.g., '/settings') */
|
|
26
26
|
initialPath?: string;
|
|
27
|
+
/**
|
|
28
|
+
* The page's own params (a `linkable` entry's pinned `params` plus what it `accepts`), e.g.
|
|
29
|
+
* `{ service: 'svc_123' }`. Added to the app's URL alongside the platform's context params, which
|
|
30
|
+
* always win (collectionId, appId, productId, proofId, isAdmin, dark, theme, parentUrl).
|
|
31
|
+
*/
|
|
32
|
+
params?: Record<string, string>;
|
|
27
33
|
/** Is user an admin of this collection */
|
|
28
34
|
isAdmin?: boolean;
|
|
29
35
|
cache?: CachedData;
|
package/docs/API_SUMMARY.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Smartlinks API Summary
|
|
2
2
|
|
|
3
|
-
Version: 2.1.
|
|
3
|
+
Version: 2.1.3 | Generated: 2026-10-26T21:00:54.277Z
|
|
4
4
|
|
|
5
5
|
This is a concise summary of all available API functions and types.
|
|
6
6
|
|
|
@@ -2585,9 +2585,11 @@ interface AppContainerComponent {
|
|
|
2585
2585
|
**DeepLinkEntry** (interface)
|
|
2586
2586
|
```typescript
|
|
2587
2587
|
interface DeepLinkEntry {
|
|
2588
|
+
id?: string;
|
|
2588
2589
|
title: string;
|
|
2589
2590
|
path?: string;
|
|
2590
2591
|
params?: Record<string, string>;
|
|
2592
|
+
accepts?: PublicViewParams;
|
|
2591
2593
|
widget?: true;
|
|
2592
2594
|
}
|
|
2593
2595
|
```
|
|
@@ -2816,6 +2818,7 @@ interface ServerFunctionBookings {
|
|
|
2816
2818
|
complete(bookingId: string): Promise<Booking>;
|
|
2817
2819
|
noShow(bookingId: string): Promise<Booking>;
|
|
2818
2820
|
freebusy(q: FreeBusyQuery): Promise<FreeBusyResponse>;
|
|
2821
|
+
message(input: MessageBookersInput): Promise<MessageBookersResult>;
|
|
2819
2822
|
slots: {
|
|
2820
2823
|
create(input: CreateSlotInput): Promise<BookingSlot>;
|
|
2821
2824
|
createMany(inputs: CreateSlotInput[], opts?: { seriesId?: string }): Promise<BookingSlot[]>;
|
|
@@ -5366,6 +5369,32 @@ interface FreeBusyResponse {
|
|
|
5366
5369
|
}
|
|
5367
5370
|
```
|
|
5368
5371
|
|
|
5372
|
+
**MessageBookersInput** (interface)
|
|
5373
|
+
```typescript
|
|
5374
|
+
interface MessageBookersInput {
|
|
5375
|
+
templateId: string
|
|
5376
|
+
channel?: 'email' | 'sms' | 'whatsapp' | 'push' | 'preferred'
|
|
5377
|
+
props?: Record<string, any>
|
|
5378
|
+
slotId?: string
|
|
5379
|
+
from?: string | Date
|
|
5380
|
+
to?: string | Date
|
|
5381
|
+
resourceType?: string
|
|
5382
|
+
resourceId?: string
|
|
5383
|
+
statuses?: BookingStatus[]
|
|
5384
|
+
ref?: string
|
|
5385
|
+
}
|
|
5386
|
+
```
|
|
5387
|
+
|
|
5388
|
+
**MessageBookersResult** (interface)
|
|
5389
|
+
```typescript
|
|
5390
|
+
interface MessageBookersResult {
|
|
5391
|
+
matched: number
|
|
5392
|
+
sent: number
|
|
5393
|
+
skipped: { noContact: number; duplicate: number; failed: number }
|
|
5394
|
+
failures: Array<{ bookingId: string; error: string }>
|
|
5395
|
+
}
|
|
5396
|
+
```
|
|
5397
|
+
|
|
5369
5398
|
**BookingAccessOptions** (interface)
|
|
5370
5399
|
```typescript
|
|
5371
5400
|
interface BookingAccessOptions {
|
|
@@ -7452,6 +7481,7 @@ interface IframeResponderOptions {
|
|
|
7452
7481
|
version?: string;
|
|
7453
7482
|
appUrl?: string;
|
|
7454
7483
|
initialPath?: string;
|
|
7484
|
+
params?: Record<string, string>;
|
|
7455
7485
|
isAdmin?: boolean;
|
|
7456
7486
|
cache?: CachedData;
|
|
7457
7487
|
onAuthLogin?: (
|
|
@@ -11902,6 +11932,9 @@ confirmed → no_show (admin).
|
|
|
11902
11932
|
**freebusy**(collectionId: string, appId: string, q: FreeBusyQuery, admin: boolean = false) → `Promise<FreeBusyResponse>`
|
|
11903
11933
|
Busy intervals per resource (bookings, blocks, slots' own blocks) — never booking details. Public only when the app sets `publicRead.bookings.freebusy: true`. GET /bookings/freebusy
|
|
11904
11934
|
|
|
11935
|
+
**message**(collectionId: string, appId: string, input: MessageBookersInput) → `Promise<MessageBookersResult>`
|
|
11936
|
+
Message the people booked (admin): a template to everyone on a slot, or everyone booked in a date range (optionally with one person / room), each send hydrated with their booking ({{ booking }}). No segment to make. POST /bookings/message
|
|
11937
|
+
|
|
11905
11938
|
### bookings.blocks
|
|
11906
11939
|
|
|
11907
11940
|
**create**(collectionId: string, appId: string, input: CreateBlockInput) → `Promise<BookingAllocation>`
|
package/docs/app-manifest.md
CHANGED
|
@@ -326,11 +326,28 @@ Static deep-linkable states built into the app — fixed routes that exist regar
|
|
|
326
326
|
|
|
327
327
|
See the [Deep Link Discovery guide](deep-link-discovery.md) for the full dual-source pattern (static manifest routes + dynamic `appConfig.linkable`).
|
|
328
328
|
|
|
329
|
+
These are the app's **pages that a host opens**: portal menus and tabs, and websites that embed the app
|
|
330
|
+
(websites.md → "Using another app's pages"). A host opens them with its own signed-in session, so one
|
|
331
|
+
app can offer several pages that each know the visitor: book, upcoming bookings, past bookings, change a
|
|
332
|
+
booking.
|
|
333
|
+
|
|
334
|
+
```json
|
|
335
|
+
"linkable": [
|
|
336
|
+
{ "id": "book", "title": "Book an appointment", "path": "/",
|
|
337
|
+
"accepts": { "optional": ["service", "staff", "date"] } },
|
|
338
|
+
{ "id": "upcoming", "title": "Your upcoming bookings", "path": "/my-bookings" },
|
|
339
|
+
{ "id": "change", "title": "Change a booking", "path": "/booking",
|
|
340
|
+
"accepts": { "required": ["id"] } }
|
|
341
|
+
]
|
|
342
|
+
```
|
|
343
|
+
|
|
329
344
|
| Field | Type | Required | Description |
|
|
330
345
|
|-------|------|----------|-------------|
|
|
346
|
+
| `id` | string | ❌ | Stable id, unique within the app: hosts and sites refer to the page by it |
|
|
331
347
|
| `title` | string | ✅ | Human-readable label shown in menus and offered to AI agents |
|
|
332
348
|
| `path` | string | ❌ | Hash route within the app (defaults to `"/"` if omitted) |
|
|
333
|
-
| `params` | object | ❌ |
|
|
349
|
+
| `params` | object | ❌ | Params this entry PINS, appended to the URL — do **not** include platform params (`collectionId`, `productId`, etc.) |
|
|
350
|
+
| `accepts` | `{ required?: string[]; optional?: string[] }` | ❌ | The params a host may supply when it opens the page (e.g. which service to start on) |
|
|
334
351
|
|
|
335
352
|
#### `publicViews`
|
|
336
353
|
|
|
@@ -371,7 +388,7 @@ An app can have any combination of three setup styles. Only public views are ext
|
|
|
371
388
|
| Style | Declared by | Lives | Context |
|
|
372
389
|
|-------|-------------|-------|---------|
|
|
373
390
|
| **Portal components** — where the app renders inside portal (collection / product / batch / variant / proof) | module registry `components.*` (set at publish) | Inside portal | Physical twin (QR/NFC scan) |
|
|
374
|
-
| **
|
|
391
|
+
| **App pages** — the entry points portal menus/tabs link to and websites embed (e.g. list vs calendar; book vs my bookings) | `linkable` / `DeepLinkEntry` | Inside a host: portal or a website | The host: its signed-in session, plus the page's `accepts` params |
|
|
375
392
|
| **Public views** — standalone external screens | `publicViews` (this block) | Outside portal | URL params only |
|
|
376
393
|
|
|
377
394
|
#### `records`
|
package/docs/bookings.md
CHANGED
|
@@ -324,8 +324,80 @@ Recorded after each write commits, for flows and app functions (`trigger: { type
|
|
|
324
324
|
`booking.no_show`, `booking.hold_expired`, `slot.full`, `slot.place_freed` (for waitlist promotion).
|
|
325
325
|
Context: `{ appId, status, slotIds }` (bookings) / `{ appId, remaining }` (slots).
|
|
326
326
|
|
|
327
|
-
Reminders before a booking (`booking.starts_in`) are coming; until then,
|
|
328
|
-
|
|
327
|
+
Reminders before a booking (`booking.starts_in`) are coming; until then, send a reminder template to
|
|
328
|
+
tomorrow's bookers with `bookings.message` (below) from a cron function.
|
|
329
|
+
|
|
330
|
+
---
|
|
331
|
+
|
|
332
|
+
## Confirmations and messages
|
|
333
|
+
|
|
334
|
+
Nothing is emailed by default: your app decides what goes out, in **flows on booking events**, with
|
|
335
|
+
**templates the business can reword in Studio** (Comms → Templates). The platform does the sending,
|
|
336
|
+
the hydration and the consent.
|
|
337
|
+
|
|
338
|
+
### Flows on booking events
|
|
339
|
+
|
|
340
|
+
A site, or an app on behalf of a business, sets them up through the cloud blueprint
|
|
341
|
+
(`cloud/flows.json` in a site; `POST /admin/collection/:cId/blueprint/apply` with
|
|
342
|
+
`{ appId, blueprint: { templates, flows } }` from an app's admin screen). Additive: re-applying never
|
|
343
|
+
removes what is there, and a template edited in Studio keeps its edits unless the blueprint sends new
|
|
344
|
+
wording.
|
|
345
|
+
|
|
346
|
+
```json
|
|
347
|
+
{
|
|
348
|
+
"templates": {
|
|
349
|
+
"booking-confirmed": { "name": "You're booked", "email": { "subject": "{{ booking.serviceName }} on {{ booking.when.date }}", "body": "<p>Hi {{ contact.firstName }}, see you at {{ booking.when.time }}.</p>" }, "sms": { "body": "Booked: {{ booking.when.shortDate }} {{ booking.when.time }}" } },
|
|
350
|
+
"booking-requested": { "name": "Request received", "email": { "subject": "We got your request", "body": "<p>We'll confirm shortly.</p>" } },
|
|
351
|
+
"staff-new-booking": { "name": "You've been booked", "email": { "subject": "New appointment", "body": "<p>{{ booking.customer.name }}, {{ booking.when.dateTime }}</p>" } }
|
|
352
|
+
},
|
|
353
|
+
"flows": [
|
|
354
|
+
{ "when": { "event": "booking.confirmed" }, "do": [{ "send": "booking-confirmed", "to": "contact" }, { "send": "staff-new-booking", "to": "resource" }, { "send": "team-new-booking", "to": "team" }] },
|
|
355
|
+
{ "when": { "event": "booking.created", "status": "pending" }, "do": [{ "send": "booking-requested", "to": "contact" }] }
|
|
356
|
+
]
|
|
357
|
+
}
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
- `when.event`: `booking.created` / `confirmed` / `cancelled` / `moved` / `completed` / `no_show` /
|
|
361
|
+
`hold_expired`, `slot.full` / `slot.place_freed`; `when.status` narrows to the booking's status at the
|
|
362
|
+
time (`"pending"` for "request received").
|
|
363
|
+
- `to`: `contact` — the booker, by their preferred channel (`channel` to force one; a template needs an
|
|
364
|
+
`sms` part to go by text); `team` — the collection's team addresses; `resource` — the booked
|
|
365
|
+
person or room: a resource that is a contact, or a record with `data.email` / `data.contactId`.
|
|
366
|
+
A **public** resource record (a staff profile the booking page lists) keeps its address on a
|
|
367
|
+
**private child record** instead (`visibility: 'admin'`, `parentType: 'record'`, `parentId` = the
|
|
368
|
+
resource) — the platform looks there too, and the address never ships with the public listing.
|
|
369
|
+
- Who gets what, in templates: `{{ booking }}` is the booking **ready-formatted**: its fields and
|
|
370
|
+
`data`, plus `serviceName`, `staffName`, `customer`, `durationMinutes`, `when` (`date`,
|
|
371
|
+
`shortDate`, `time`, `endTime`, `dateTime`, `timezone` — in the business's timezone) and, when the
|
|
372
|
+
recipient's timezone is known and differs, `whenFor` (the same in theirs); `editToken` only when the
|
|
373
|
+
recipient is the booker (a guest's manage link). `{{ contact }}` is the recipient; `{{ appConfig }}`
|
|
374
|
+
the app's public config (a booking page's address, the business name) for sends to the contact.
|
|
375
|
+
|
|
376
|
+
### Who is reachable
|
|
377
|
+
|
|
378
|
+
A **guest** who books with an email or phone gets a **contact** (found by that identity or made,
|
|
379
|
+
filling only empty fields: a form's details never overwrite a known contact), and so does a customer
|
|
380
|
+
**staff book in by hand** (`customer: { name, email, phone }` on the body or in `data`). Admin
|
|
381
|
+
creates may pass `contactId` instead. A booking with no contact gets no messages: show the booker why
|
|
382
|
+
an email is worth giving.
|
|
383
|
+
|
|
384
|
+
### Messaging everyone booked — `bookings.message`
|
|
385
|
+
|
|
386
|
+
No list to build, no segment to save: send a template to everyone on a slot, or everyone booked in a
|
|
387
|
+
window (optionally with one resource), each hydrated with **their own** booking.
|
|
388
|
+
|
|
389
|
+
```ts
|
|
390
|
+
// Everyone on tomorrow's 10:00 class
|
|
391
|
+
await SL.bookings.message(collectionId, appId, { templateId, slotId })
|
|
392
|
+
// Everyone booked with Sam next week, by email even if they prefer texts
|
|
393
|
+
await SL.bookings.message(collectionId, appId, { templateId, channel: 'email', from, to, resourceType: 'record', resourceId: samId })
|
|
394
|
+
// → { matched, sent, skipped: { noContact, duplicate, failed }, failures: [{ bookingId, reason }] }
|
|
395
|
+
```
|
|
396
|
+
|
|
397
|
+
Statuses default to held, pending and confirmed; at most 500 recipients per call (narrow the window
|
|
398
|
+
for more). One message per contact on a slot, one per booking in a window. Also
|
|
399
|
+
`ctx.sl.bookings.message(input)` in a server function (`sl:bookings:write`) — a cron function that
|
|
400
|
+
messages tomorrow's bookers is a reminder system in five lines.
|
|
329
401
|
|
|
330
402
|
---
|
|
331
403
|
|
|
@@ -343,6 +415,7 @@ All take `(collectionId, appId, …)`; `admin` is the last parameter where both
|
|
|
343
415
|
| `bookings.confirm / cancel / move(…, admin?, { editToken }?)` | ✓ | per `ownerActions.bookings` |
|
|
344
416
|
| `bookings.extendHold / complete / noShow` | ✓ | — |
|
|
345
417
|
| `bookings.freebusy(q, admin?)` | ✓ | if `publicRead.bookings.freebusy` |
|
|
418
|
+
| `bookings.message(input)` | ✓ | — |
|
|
346
419
|
| `bookings.slots.create / createMany / update / remove` | ✓ | — |
|
|
347
420
|
| `bookings.slots.list / get(…, admin?)` | ✓ | open, public slots |
|
|
348
421
|
| `bookings.blocks.create / list / remove` | ✓ | — |
|
|
@@ -172,6 +172,9 @@ Both sources use the same entry shape:
|
|
|
172
172
|
|
|
173
173
|
```typescript
|
|
174
174
|
interface DeepLinkEntry {
|
|
175
|
+
/** Stable id, unique within the app (optional): hosts and websites refer to the page by it */
|
|
176
|
+
id?: string;
|
|
177
|
+
|
|
175
178
|
/** Human-readable label for this state (required) */
|
|
176
179
|
title: string;
|
|
177
180
|
|
|
@@ -193,6 +196,13 @@ interface DeepLinkEntry {
|
|
|
193
196
|
*/
|
|
194
197
|
params?: Record<string, string>;
|
|
195
198
|
|
|
199
|
+
/**
|
|
200
|
+
* The params a host may supply when it opens this page (optional), e.g. a booking page that can
|
|
201
|
+
* start on one service: `{ optional: ['service', 'staff', 'date'] }`. `params` are pinned by the
|
|
202
|
+
* entry; `accepts` are filled in by whoever opens it.
|
|
203
|
+
*/
|
|
204
|
+
accepts?: { required?: string[]; optional?: string[] };
|
|
205
|
+
|
|
196
206
|
/**
|
|
197
207
|
* When `true`, this entry is also available as a dynamic data context for widgets.
|
|
198
208
|
*
|
|
@@ -211,7 +221,9 @@ interface DeepLinkEntry {
|
|
|
211
221
|
|-------|----------|-------|
|
|
212
222
|
| `title` | ✅ | Displayed in menus and offered to AI agents. Should be concise and human-readable. |
|
|
213
223
|
| `path` | ❌ | Hash route within the app. Defaults to `/` if omitted. |
|
|
224
|
+
| `id` | ❌ | Stable id for hosts and websites to refer to the page by. |
|
|
214
225
|
| `params` | ❌ | App-specific query params only. Platform params are injected automatically. |
|
|
226
|
+
| `accepts` | ❌ | The params whoever opens the page may supply (`required` / `optional`). |
|
|
215
227
|
| `widget` | ❌ | Set to `true` to make this entry selectable in the widget config picker as a dynamic data context. |
|
|
216
228
|
|
|
217
229
|
> **Tip:** Different entries can share the same `path` if they differ by `params` — for example, two entries pointing at `/settings` with different `tab` params.
|
package/docs/iframe-responder.md
CHANGED
|
@@ -103,6 +103,7 @@ interface IframeResponderOptions {
|
|
|
103
103
|
version?: 'stable' | 'development'; // App version (default: 'stable')
|
|
104
104
|
appUrl?: string; // Override URL (for local dev)
|
|
105
105
|
initialPath?: string; // Initial hash path (e.g., '/settings')
|
|
106
|
+
params?: Record<string, string>; // The page's own params (e.g. { service: 'svc_123' }); context params win
|
|
106
107
|
|
|
107
108
|
// Data
|
|
108
109
|
cache?: CachedData; // Pre-cached data
|
package/docs/server-functions.md
CHANGED
|
@@ -517,7 +517,7 @@ Prefer the platform primitives above over a dependency; when you do need one, pi
|
|
|
517
517
|
| Check a tapped tag is real | `ctx.sl.tags.resolve(proofId)` → `{ id, claimSetId, code }` or `null` | `sl:tags:read` |
|
|
518
518
|
| Put a visitor's details on a contact | `ctx.sl.contacts.upsert({ email, firstName, … })` → `{ contactId, created }` (fills empty fields only) | `sl:contacts:write` |
|
|
519
519
|
| Record "they did X" | `ctx.sl.interactions.record({ interactionId, contactId, outcome, metadata })` | `sl:interactions:write` |
|
|
520
|
-
| Take a booking with your own checks (hours, queue token, payment) | `ctx.sl.bookings.create({ slotId \| resource + time, contactId, status, idempotencyKey })` — never oversold, never double-booked; also `confirm` / `cancel` / `move` / `mine` / `freebusy` / `slots.*` / `blocks.*` (see [bookings.md](bookings.md)) | `sl:bookings:write` (`sl:bookings:read` to read) |
|
|
520
|
+
| Take a booking with your own checks (hours, queue token, payment) | `ctx.sl.bookings.create({ slotId \| resource + time, contactId, status, idempotencyKey })` — never oversold, never double-booked; also `confirm` / `cancel` / `move` / `mine` / `freebusy` / `message` (everyone booked, hydrated) / `slots.*` / `blocks.*` (see [bookings.md](bookings.md)) | `sl:bookings:write` (`sl:bookings:read` to read) |
|
|
521
521
|
|
|
522
522
|
Notes:
|
|
523
523
|
- **Talking to the SmartLinks core is `ctx.sl`, not HTTP.** The common pattern — *validate /
|
|
@@ -636,6 +636,18 @@ try {
|
|
|
636
636
|
}
|
|
637
637
|
```
|
|
638
638
|
|
|
639
|
+
`ctx.sl.bookings.message({ templateId, slotId | from, to, resourceType?, resourceId?, channel? })` sends a
|
|
640
|
+
template to everyone booked (each hydrated with their own booking) — the five-line reminder cron:
|
|
641
|
+
|
|
642
|
+
```ts
|
|
643
|
+
// manifest: { name: 'remind', trigger: { type: 'cron', schedule: '0 17 * * *' }, capabilities: ['sl:bookings:write'] }
|
|
644
|
+
export async function remind(ctx) {
|
|
645
|
+
const from = new Date(); from.setDate(from.getDate() + 1); from.setHours(0, 0, 0, 0)
|
|
646
|
+
const to = new Date(from); to.setDate(to.getDate() + 1)
|
|
647
|
+
return ctx.sl.bookings.message({ templateId: 'bp.' + ctx.collectionId + '.reminder', from, to })
|
|
648
|
+
}
|
|
649
|
+
```
|
|
650
|
+
|
|
639
651
|
The local harness runs bookings in memory with the same rules (capacity, per-person limits, no
|
|
640
652
|
overlaps, idempotency, holds): `ctx.memory.bookings`.
|
|
641
653
|
|
package/docs/websites.md
CHANGED
|
@@ -66,6 +66,32 @@ bytes. `_server/` is never served to visitors.
|
|
|
66
66
|
doing work that belongs in a server function or a scheduled job.
|
|
67
67
|
- **No secrets** in the server bundle and no Node built-ins beyond `nodejs_compat`.
|
|
68
68
|
|
|
69
|
+
## Using another app's pages
|
|
70
|
+
|
|
71
|
+
Some apps do a job a site shouldn't rebuild: a booking journey holds the time rules, the clash
|
|
72
|
+
handling and the confirmation; "my bookings" holds who may change what. Those apps declare their pages
|
|
73
|
+
in `linkable` (app-manifest.md), each with a stable `id`, a `path` and the params it `accepts`. A site
|
|
74
|
+
embeds one with **`IframeResponder`** (iframe-responder.md), from the browser:
|
|
75
|
+
|
|
76
|
+
```typescript
|
|
77
|
+
const site = SL.site.current()
|
|
78
|
+
const responder = new SL.IframeResponder({
|
|
79
|
+
collectionId: site.collectionId,
|
|
80
|
+
appId: 'appointments', // the app's id on this collection
|
|
81
|
+
initialPath: '/my-bookings', // the page's `path`
|
|
82
|
+
params: { service: 'svc_123' }, // what the page `accepts`
|
|
83
|
+
onResize: (h) => { iframe.style.height = `${h}px` },
|
|
84
|
+
})
|
|
85
|
+
iframe.src = await responder.attach(iframe)
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
- **The visitor's session goes with it.** The page's API calls are proxied through the site, so it
|
|
89
|
+
acts as whoever is signed in to the site (or as a guest). Never pass a token in the URL.
|
|
90
|
+
- **The app must be installed** on the site's collection; its address comes from there.
|
|
91
|
+
- **Show the app's content natively where you can** (its `headless` data: services, staff, events)
|
|
92
|
+
for your own design and SEO, and embed the page only for the part that does the work.
|
|
93
|
+
- Pages render on the client, after load: they aren't part of the server-rendered HTML.
|
|
94
|
+
|
|
69
95
|
## Caching
|
|
70
96
|
|
|
71
97
|
The router keeps a rendered page at the edge when your response says it may:
|
package/openapi.yaml
CHANGED
|
@@ -10291,6 +10291,32 @@ paths:
|
|
|
10291
10291
|
description: Unauthorized
|
|
10292
10292
|
404:
|
|
10293
10293
|
description: Not found
|
|
10294
|
+
/message:
|
|
10295
|
+
post:
|
|
10296
|
+
tags:
|
|
10297
|
+
- bookings
|
|
10298
|
+
summary: "Message the people booked (admin): a template to everyone on a slot, or everyone booked in a date range (optionally with one person / room), each send hydrated with their booking ({{ booking }})."
|
|
10299
|
+
operationId: bookings_message
|
|
10300
|
+
security: []
|
|
10301
|
+
responses:
|
|
10302
|
+
200:
|
|
10303
|
+
description: Success
|
|
10304
|
+
content:
|
|
10305
|
+
application/json:
|
|
10306
|
+
schema:
|
|
10307
|
+
$ref: "#/components/schemas/MessageBookersResult"
|
|
10308
|
+
400:
|
|
10309
|
+
description: Bad request
|
|
10310
|
+
401:
|
|
10311
|
+
description: Unauthorized
|
|
10312
|
+
404:
|
|
10313
|
+
description: Not found
|
|
10314
|
+
requestBody:
|
|
10315
|
+
required: true
|
|
10316
|
+
content:
|
|
10317
|
+
application/json:
|
|
10318
|
+
schema:
|
|
10319
|
+
$ref: "#/components/schemas/MessageBookersInput"
|
|
10294
10320
|
/platform/location:
|
|
10295
10321
|
post:
|
|
10296
10322
|
tags:
|
|
@@ -19883,6 +19909,8 @@ components:
|
|
|
19883
19909
|
DeepLinkEntry:
|
|
19884
19910
|
type: object
|
|
19885
19911
|
properties:
|
|
19912
|
+
id:
|
|
19913
|
+
type: string
|
|
19886
19914
|
title:
|
|
19887
19915
|
type: string
|
|
19888
19916
|
path:
|
|
@@ -19891,6 +19919,8 @@ components:
|
|
|
19891
19919
|
type: object
|
|
19892
19920
|
additionalProperties:
|
|
19893
19921
|
type: string
|
|
19922
|
+
accepts:
|
|
19923
|
+
$ref: "#/components/schemas/PublicViewParams"
|
|
19894
19924
|
widget:
|
|
19895
19925
|
type: object
|
|
19896
19926
|
additionalProperties: true
|
|
@@ -24706,6 +24736,62 @@ components:
|
|
|
24706
24736
|
- from
|
|
24707
24737
|
- to
|
|
24708
24738
|
- resources
|
|
24739
|
+
MessageBookersInput:
|
|
24740
|
+
type: object
|
|
24741
|
+
properties:
|
|
24742
|
+
templateId:
|
|
24743
|
+
type: string
|
|
24744
|
+
channel:
|
|
24745
|
+
type: string
|
|
24746
|
+
enum:
|
|
24747
|
+
- email
|
|
24748
|
+
- sms
|
|
24749
|
+
- whatsapp
|
|
24750
|
+
- push
|
|
24751
|
+
- preferred
|
|
24752
|
+
props:
|
|
24753
|
+
type: object
|
|
24754
|
+
additionalProperties: true
|
|
24755
|
+
slotId:
|
|
24756
|
+
type: string
|
|
24757
|
+
from:
|
|
24758
|
+
type: object
|
|
24759
|
+
additionalProperties: true
|
|
24760
|
+
to:
|
|
24761
|
+
type: object
|
|
24762
|
+
additionalProperties: true
|
|
24763
|
+
resourceType:
|
|
24764
|
+
type: string
|
|
24765
|
+
resourceId:
|
|
24766
|
+
type: string
|
|
24767
|
+
statuses:
|
|
24768
|
+
type: array
|
|
24769
|
+
items:
|
|
24770
|
+
$ref: "#/components/schemas/BookingStatus"
|
|
24771
|
+
ref:
|
|
24772
|
+
type: string
|
|
24773
|
+
required:
|
|
24774
|
+
- templateId
|
|
24775
|
+
MessageBookersResult:
|
|
24776
|
+
type: object
|
|
24777
|
+
properties:
|
|
24778
|
+
matched:
|
|
24779
|
+
type: number
|
|
24780
|
+
sent:
|
|
24781
|
+
type: number
|
|
24782
|
+
skipped:
|
|
24783
|
+
type: object
|
|
24784
|
+
additionalProperties: true
|
|
24785
|
+
failures:
|
|
24786
|
+
type: array
|
|
24787
|
+
items:
|
|
24788
|
+
type: object
|
|
24789
|
+
additionalProperties: true
|
|
24790
|
+
required:
|
|
24791
|
+
- matched
|
|
24792
|
+
- sent
|
|
24793
|
+
- skipped
|
|
24794
|
+
- failures
|
|
24709
24795
|
BookingAccessOptions:
|
|
24710
24796
|
type: object
|
|
24711
24797
|
properties:
|
|
@@ -27570,6 +27656,10 @@ components:
|
|
|
27570
27656
|
type: string
|
|
27571
27657
|
initialPath:
|
|
27572
27658
|
type: string
|
|
27659
|
+
params:
|
|
27660
|
+
type: object
|
|
27661
|
+
additionalProperties:
|
|
27662
|
+
type: string
|
|
27573
27663
|
isAdmin:
|
|
27574
27664
|
type: boolean
|
|
27575
27665
|
cache:
|