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.

Accepts either credential in X-Api-Key: the broad workspace key (server-side), or the workspace's scoped brandfine_pk_… publishable key (safe for fully-static sites — it is read-only here; it cannot book, look up, or cancel).

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
visitorNamestring✓1–120 chars.
visitorEmailstring✓Server-validated. Up to 200 chars. Used as the destination for the "request received" + confirmation/decline emails.
visitorPhonestring—Up to 40 chars. Free-text — no formatting enforced.
visitorMessagestring—Up to 2,000 chars. Shown to the customer in the inbox row.
requestedAtstring✓UTC ISO 8601 of the slot start. Must match one of the slots from GET /availability.
visitorSessionIdstring—Up to 120 chars. Cookie-derived session id from your frontend — an unverified soft link across page loads. For real identity use visitor below.
visitorobject—Signed host-app identity: { externalId, name?, email?, attributes?, identityToken } — the SAME shape and HMAC Live Chat accepts (identityToken = HMAC_SHA256(identitySecret, externalId), hex; the zero-config secret derives from your workspace API key, so liveChat.identityToken() signs for both plugins). Valid → the booking is attributed: verified name/email take precedence, identityVerified: true, and the shared externalId links it to the person's chat threads. Invalid or absent → the booking proceeds anonymously; never a 400. Caps: externalId ≤128, name ≤120, email ≤200, ≤10 attributes (keys ≤40, values ≤100).

Response — 201 Created

{
  "id": "cln9b2k8w0001abc",
  "createdAt": "2026-06-04T15:30:00.000Z",
  "requestedAt": "2026-06-08T06:00:00.000Z",
  "durationMinutes": 30,
  "status": "PENDING",
  "identityVerified": true,
  "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.
identityVerifiedbooleanTrue iff visitor.identityToken HMAC-verified — your signal that the booking landed attributed rather than anonymous.
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.


POST /external/appointments/requests/lookup

Identity-scoped read-back — a signed host app fetches the verified person's own appointments ("where does my request stand?"). POST rather than GET so the identityToken never lands in proxy logs or browser history. Broad workspace key only.

Request

POST /external/appointments/requests/lookup HTTP/1.1
Host: api.brandfine.co
X-Api-Key: bfwk_xxx
Content-Type: application/json

{
  "externalId": "crm-user:abc123",
  "identityToken": "hex-hmac…",
  "id": "cln9b2k8w0001abc"
}
FieldTypeRequiredNotes
externalIdstring✓The person's host-app id — must HMAC-verify against identityToken.
identityTokenstring✓Same signature as createRequest.visitor.identityToken. Unlike booking, a bad signature here is a hard 401 — reads require proof.
idstring—Narrow to one appointment. 404 when the id doesn't exist or belongs to a different person — indistinguishable by design.

Response — 200 OK

{
  "timezone": "Europe/Istanbul",
  "appointments": [
    {
      "id": "cln9b2k8w0001abc",
      "status": "CONFIRMED",
      "requestedAt": "2026-06-08T06:00:00.000Z",
      "scheduledAt": "2026-06-08T06:00:00.000Z",
      "durationMinutes": 30,
      "timezone": "Europe/Istanbul",
      "declineReason": null,
      "rescheduleCount": 0,
      "respondedAt": "2026-06-05T09:12:00.000Z",
      "createdAt": "2026-06-04T15:30:00.000Z"
    }
  ]
}

Newest first, capped at 50. scheduledAt is non-null only once CONFIRMED (after a reschedule it reflects the current slot); declineReason is the customer's note, populated only on REJECTED. Only bookings created with a verified identity appear here — anonymous bookings have no externalId to match.

Errors

  • 400 Bad Request — missing/oversized fields.
  • 401 Unauthorized — invalid X-Api-Key, or the identityToken failed verification.
  • 404 Not Found — id given and no matching appointment for this person.

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