Appointments
Public booking endpoints for workspaces running the Brandfine Appointments plugin — read availability, submit visitor requests, look up + cancel by token.
Four public endpoints under /external/appointments/*. Three are
workspace-API-key authenticated (X-Api-Key); the visitor-facing
lookup + cancel use a one-time cancellation token instead.
| Method | Path | Auth | Status |
|---|---|---|---|
GET | /external/appointments/availability | X-Api-Key | v1 — returns bookable slots inside the workspace's booking window. |
POST | /external/appointments/requests | X-Api-Key | v1 — submits a visitor's appointment request. Server validates the slot. |
GET | /external/appointments/requests/:token | token in path | Dormant — not called by any first-party SDK or widget. See note in the section below. |
POST | /external/appointments/requests/:token/cancel | token in path | Dormant — same as above. |
All endpoints inherit the permissive /external/* CORS — any
origin may call them with Content-Type: application/json +
X-Api-Key. In practice, call them server-side. The workspace
API key is broad-scope (also reads posts, navigations, contact
submissions, analytics), so embedding it in browser code exposes
much more than just appointments. Your frontend hits your
backend; your backend hits Brandfine.
The endpoints respond with {"enabled": false, ...}-shaped
responses (instead of throwing) when a workspace has not activated
the Appointments plugin or has toggled "Accepting bookings" off,
so consumer UIs can gracefully degrade.
GET /external/appointments/availability
Returns the slots a visitor can book inside the workspace's configured window. The server already applies business hours, lead time, booking window, and busy-range filtering — render the response as-is.
Query params
| Param | Type | Notes |
|---|---|---|
from | ISO 8601 string | Optional clamp. Slots before this are excluded. Ignored if it's before the workspace's lead-time floor (server picks the later of the two). |
to | ISO 8601 string | Optional clamp. Slots after this are excluded. Ignored if it's after the workspace's booking-window ceiling. |
Request
GET /external/appointments/availability HTTP/1.1
Host: api.brandfine.co
X-Api-Key: bfwk_xxx
Accept: application/jsonResponse — 200 OK
{
"enabled": true,
"timezone": "Europe/Istanbul",
"slotDurationMinutes": 30,
"leadTimeHours": 24,
"bookingWindowDays": 14,
"policyText": "Please give 24h notice for cancellations.",
"slots": [
{ "start": "2026-06-08T06:00:00.000Z", "end": "2026-06-08T06:30:00.000Z" },
{ "start": "2026-06-08T06:30:00.000Z", "end": "2026-06-08T07:00:00.000Z" }
],
"windowStart": "2026-06-05T12:00:00.000Z",
"windowEnd": "2026-06-19T12:00:00.000Z"
}| Field | Type | Notes |
|---|---|---|
enabled | boolean | false = plugin not activated for this workspace, or customer toggled bookings off. Render a "not accepting bookings" state. |
timezone | string | IANA timezone the workspace's business hours are configured in. Render visitor slots in their LOCAL timezone; show this as a small "(host's time: HH:MM)" subtext. |
slotDurationMinutes | number | Always 15/30/45/60 in v1. |
leadTimeHours | number | Minimum notice the customer requires. Slots within this window are already filtered out. |
bookingWindowDays | number | How far ahead booking is open. Slots beyond this date are filtered out. |
policyText | string | null | Free-text policy the customer wrote — surface verbatim before the visitor submits. |
slots[] | array | UTC ISO 8601 starts + ends, ordered chronologically. Skipped: confirmed-conflict slots, in-the-past slots, lead-time slots, outside-business-hours slots. |
windowStart / windowEnd | string | UTC ISO 8601. The effective range the slots cover after lead-time + booking-window clamps. |
Response — 200 OK (plugin disabled)
{
"enabled": false,
"timezone": "UTC",
"slotDurationMinutes": 30,
"leadTimeHours": 0,
"bookingWindowDays": 14,
"policyText": null,
"slots": [],
"windowStart": "2026-06-04T07:00:00.000Z",
"windowEnd": "2026-06-04T07:00:00.000Z"
}Same shape, empty slots. Do not throw in your UI — render
a "not accepting bookings right now" state instead.
Errors
- 401 Unauthorized — missing or invalid
X-Api-Key.
POST /external/appointments/requests
Submits a visitor's request for a specific slot. The server re-validates the slot is still bookable (business hours, lead time, not in the past, no overlap with confirmed appointments) before accepting.
Request
POST /external/appointments/requests HTTP/1.1
Host: api.brandfine.co
X-Api-Key: bfwk_xxx
Content-Type: application/json
{
"visitorName": "Visitor Name",
"visitorEmail": "visitor@example.com",
"visitorPhone": "+1 555 0100",
"visitorMessage": "Looking to discuss the product.",
"requestedAt": "2026-06-08T06:00:00.000Z",
"visitorSessionId": "abc123"
}Fields
| Field | Type | Required | Notes |
|---|---|---|---|
visitorName | string | ✓ | 1–120 chars. |
visitorEmail | string | ✓ | Server-validated. Up to 200 chars. Used as the destination for the "request received" + confirmation/decline emails. |
visitorPhone | string | — | Up to 40 chars. Free-text — no formatting enforced. |
visitorMessage | string | — | Up to 2,000 chars. Shown to the customer in the inbox row. |
requestedAt | string | ✓ | UTC ISO 8601 of the slot start. Must match one of the slots from GET /availability. |
visitorSessionId | string | — | Up to 120 chars. Cookie-derived session id from your frontend, lets a returning visitor look up their own requests later. |
Response — 201 Created
{
"id": "cln9b2k8w0001abc",
"createdAt": "2026-06-04T15:30:00.000Z",
"requestedAt": "2026-06-08T06:00:00.000Z",
"durationMinutes": 30,
"status": "PENDING",
"cancellationToken": "f8d2c1e3a4b5..."
}| Field | Type | Notes |
|---|---|---|
id | string | Request id. |
createdAt | string | ISO 8601 timestamp the request was persisted. |
requestedAt | string | Echoed back as canonical UTC. |
durationMinutes | number | Slot duration the workspace had configured at submission time. |
status | string | Always "PENDING" on creation. |
cancellationToken | string | null | One-time-use token for visitor self-cancellation. Embed in your confirmation email / "your appointment" page. |
The server immediately fires two emails (fire-and-forget — failure doesn't fail your request):
- Visitor — "request received, you'll hear back".
- Customer — "new request for X, review in inbox" — sent to
the plugin's configured
notificationEmail, or the team's notification settings if none.
Errors
- 400 Bad Request — validation failed (missing required field, invalid email, malformed
requestedAt). - 401 Unauthorized — missing or invalid
X-Api-Key. - 404 Not Found — workspace doesn't have Appointments activated, OR the slot is outside business hours / lead time / booking window, OR the slot was just taken between availability fetch and submit. The body's
messagedistinguishes the cases ("not accepting...", "outside business hours...", "just taken...").
Retry policy: don't retry on 400 or 404. The slot won't appear
in /availability again if it's now busy; re-fetch availability
and let the visitor pick a different slot. Do retry on 5xx +
network errors with exponential backoff.
GET /external/appointments/requests/:token
Dormant in v1. The endpoint exists but isn't called by any first-party SDK or widget in v1. The visitor's only browser-side interaction is the initial request submission; all subsequent status changes (approve / decline / reschedule) are driven by the customer in the CMS and communicated via email. This endpoint is kept available for possible future reschedule-respond flows where the email contains a link the visitor clicks to accept a proposed new time.
Visitor-facing lookup. The token IS the auth — no X-Api-Key
required. Returns just enough to render a "your appointment is
confirmed for X" or "this request was already cancelled" page,
without leaking other workspace data.
Request
GET /external/appointments/requests/f8d2c1e3a4b5... HTTP/1.1
Host: api.brandfine.co
Accept: application/jsonResponse — 200 OK
{
"id": "cln9b2k8w0001abc",
"visitorName": "Visitor Name",
"visitorEmail": "visitor@example.com",
"requestedAt": "2026-06-08T06:00:00.000Z",
"durationMinutes": 30,
"status": "PENDING",
"workspace": { "name": "Touchwise", "slug": "touchwise" }
}status may be any of PENDING | CONFIRMED | REJECTED | CANCELLED. Render accordingly:
| Status | UI suggestion |
|---|---|
PENDING | Show "Cancel" button + the requested time. |
CONFIRMED | Show "Confirmed for X". No cancel button (token is revoked). |
REJECTED | Show "Unfortunately X was declined". |
CANCELLED | Show "Already cancelled". |
Errors
- 404 Not Found — token unknown, revoked (already-responded request), or already-consumed (cancelled).
POST /external/appointments/requests/:token/cancel
Dormant in v1. Same status as the lookup endpoint above — the endpoint exists but isn't surfaced through any first-party SDK or widget. Cancellation in v1 is customer-driven from the CMS; no visitor self-cancel on the customer's site.
Visitor self-cancellation. Only works while the request is still
PENDING — once the customer has confirmed or declined, the
token is revoked.
Request
POST /external/appointments/requests/f8d2c1e3a4b5.../cancel HTTP/1.1
Host: api.brandfine.co
Content-Type: application/json
{}(Empty body. The token in the URL IS the credential.)
Response — 200 OK
{ "id": "cln9b2k8w0001abc", "status": "CANCELLED" }Errors
- 400 Bad Request — request is no longer in
PENDING(already confirmed, rejected, or cancelled). - 404 Not Found — token unknown or already revoked.
Where appointment requests show up in the CMS
CMS sidebar → Plugins → Appointments. The inbox aggregates across every workspace in the active team with workspace chips on each row. Each row's Confirm / Decline action fires the corresponding visitor email and clears the cancellation token.
Per-workspace settings (business hours, slot duration, timezone, notification email, policy text) live in the Settings dialog from either the catalog card's "Manage settings" button or the inbox's top-right Settings button.
SDK
The bf.appointments namespace wraps
all four endpoints with typed inputs / outputs. Use it instead of
calling these endpoints directly from TypeScript.