Brandfine Docs
REST API

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.

MethodPathAuthStatus
GET/external/appointments/availabilityX-Api-Keyv1 — returns bookable slots inside the workspace's booking window.
POST/external/appointments/requestsX-Api-Keyv1 — submits a visitor's appointment request. Server validates the slot.
GET/external/appointments/requests/:tokentoken in pathDormant — not called by any first-party SDK or widget. See note in the section below.
POST/external/appointments/requests/:token/canceltoken in pathDormant — 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

ParamTypeNotes
fromISO 8601 stringOptional clamp. Slots before this are excluded. Ignored if it's before the workspace's lead-time floor (server picks the later of the two).
toISO 8601 stringOptional 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/json

Response — 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"
}
FieldTypeNotes
enabledbooleanfalse = plugin not activated for this workspace, or customer toggled bookings off. Render a "not accepting bookings" state.
timezonestringIANA 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.
slotDurationMinutesnumberAlways 15/30/45/60 in v1.
leadTimeHoursnumberMinimum notice the customer requires. Slots within this window are already filtered out.
bookingWindowDaysnumberHow far ahead booking is open. Slots beyond this date are filtered out.
policyTextstring | nullFree-text policy the customer wrote — surface verbatim before the visitor submits.
slots[]arrayUTC ISO 8601 starts + ends, ordered chronologically. Skipped: confirmed-conflict slots, in-the-past slots, lead-time slots, outside-business-hours slots.
windowStart / windowEndstringUTC 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

FieldTypeRequiredNotes
visitorNamestring1–120 chars.
visitorEmailstringServer-validated. Up to 200 chars. Used as the destination for the "request received" + confirmation/decline emails.
visitorPhonestringUp to 40 chars. Free-text — no formatting enforced.
visitorMessagestringUp to 2,000 chars. Shown to the customer in the inbox row.
requestedAtstringUTC ISO 8601 of the slot start. Must match one of the slots from GET /availability.
visitorSessionIdstringUp 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..."
}
FieldTypeNotes
idstringRequest id.
createdAtstringISO 8601 timestamp the request was persisted.
requestedAtstringEchoed back as canonical UTC.
durationMinutesnumberSlot duration the workspace had configured at submission time.
statusstringAlways "PENDING" on creation.
cancellationTokenstring | nullOne-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):

  1. Visitor — "request received, you'll hear back".
  2. 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 message distinguishes 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/json

Response — 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:

StatusUI suggestion
PENDINGShow "Cancel" button + the requested time.
CONFIRMEDShow "Confirmed for X". No cancel button (token is revoked).
REJECTEDShow "Unfortunately X was declined".
CANCELLEDShow "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.

On this page