# Booking integration (nisebookings)

This project integrates **nisebookings** — a hosted booking service (appointments, group
sessions, recurring series, prepaid packages, waiting lists, event tickets) at
`https://app.nisebookings.com`, reached with the account's **public key**. Customer-facing widget text is Dutch and English; the API is JSON.

## Decide the path first

| Situation | Path |
|---|---|
| Normal web app/site, standard flow | **Embed the widget** — two lines of HTML, no build step |
| WordPress | the nisebookings plugin (listed as **nise booking**), shortcodes `[nise_booking]` / `[nise_events]` |
| Custom UI, mobile app, server-side flows, cart/series/tickets logic | **Public REST API** |
| Create/configure services, staff, schedules, opening hours from code | **Management API** with a scoped key (admin → Instellingen → API): `POST /api/v1/services`, `PUT /api/v1/staff/{id}/schedule`, `PATCH /api/v1/settings`, … Start with https://guide.nisebookings.com/api/ ; spec at https://guide.nisebookings.com/api/openapi.yaml. Ask the human for the key — it is shown once at creation. |
| React to bookings, cancellations, tickets or payments in your own system | **Webhooks**: `POST /api/v1/webhooks {url, events}` (scope `settings:write`) → signed JSON per event (Standard Webhooks headers; secret `whsec_…` shown once). Verify the signature on the raw body, dedupe on `webhook-id`, answer 2xx within 10 s. https://guide.nisebookings.com/10-webhooks.html |
| Operate the account conversationally (plan appointments, look up customers, change settings) | the hosted **MCP server** `https://app.nisebookings.com/api/mcp` with the same key as `Authorization: Bearer nk_…`; setup per agent: https://guide.nisebookings.com/api/mcp.html |

Check fit before building: day-based rentals (a boat, a room, a holiday home) **are** supported —
`durationDays` 1–365 on the service, the widget asks for a start date and prices per day — but one
rate per day (no seasons or minimum stays) and a booking's span is fixed once made. Not supported:
two-way staff calendar sync, SMS, conditional intake questions (plain per-service questions and file
attachments are), and customer-facing texts beyond Dutch and English (the widget ships both; the
mails are Dutch). See https://guide.nisebookings.com/09-when-to-use.html.

## Embed

```html
<script src="https://app.nisebookings.com/widget.js" defer></script>
<nise-booking data-key="<account public key>"></nise-booking>
```

Optional attributes: `data-api` (only if the script is hosted elsewhere), `data-service`,
`data-staff="<id|any>"`, `data-package="<token>"` (plan a bought package), `data-lang="nl|en"`
(default: the account's language; the widget ships Dutch and English). Events:
`<script src="https://app.nisebookings.com/events.js" defer></script>` + `<nise-events data-key="…">`.
The element renders in a shadow DOM; theme it with `--nb-*` CSS custom properties; it has no JS API.
Both scripts log `nisebookings@<version>` on load; `?v=<version>` on the script URL is a supported cache-bust.

## API essentials

Base: `https://app.nisebookings.com/api/public/<public key>`

| Call | Purpose |
|---|---|
| `GET /services` | account config + catalogue (ids, prices, extras, staff per service) |
| `GET /slots?service=&staff=&from=YYYY-MM-DD&days=7&persons=1` | free times |
| `POST /slots` `{items[], hold[], from, days}` | chain search, cart holds |
| `POST /recurrence` `{item, rule, hold}` | plan a repeating series |
| `POST /quote` `{items[], coupon?, payFull?, gateway?}` | authoritative price |
| `POST /bookings` `{items[], series?, name, email, phone, notes, gateway, coupon?, payFull?, returnUrl?}` | create the order |
| `GET /payments/<token>` | payment status (poll after a Mollie redirect) |
| `GET /packages/<token>` | package overview for a personal link |
| `POST /bookings/<token>/reschedule` `{start, staff?}` | move the customer's own booking (magic-link token) to another offered time |
| `GET /events` · `POST /events/<id>/tickets` | ticket shop |

https://guide.nisebookings.com/openapi.yaml is the machine-readable version of this table.

## Non-negotiables

- **Never compute** availability, price, deposit, discount, capacity or duration locally. The server
  recomputes everything itself; client numbers are ignored.
- **Send back the exact `start` and `staffId`** from a slot search. Times are UTC ISO strings;
  `from`/`until` are dates in the account's time zone; display in `tenant.timeZone`.
- **One checkout = one order = one payment** (carts, chains, series and compound services included).
- **Errors:** `409 slot_unavailable` (+`item` index) → re-fetch slots and let the user pick again.
  `422 validation` (+`fields`) → show the field errors. `502 payment_unavailable` → the order was
  rejected; offer pay-on-site or retry. `404` = wrong key/service/token.
- **Retries:** send an `Idempotency-Key: <uuid>` header on `POST /bookings` and `POST /events/<id>/tickets`
  (one key per checkout attempt, generated before the first try). On a timeout or 5xx, resend the
  same body with the same key: you get the original response (`Idempotent-Replayed: true`), never a
  second order. `409 idempotency_in_progress` → wait 1 s and resend; `422 idempotency_mismatch` → you
  changed the body, use a new key. Without the header a retry books twice.
- **Secrets:** the public key is not a secret (it lives in browser code). A management API key
  (`nk_…`) and the Mollie key are: never put them in browser code or commit them; a management key
  belongs only in server-side configuration.
- **Cross-origin:** the embedding origin must be in the account's allowed-sites list (admin settings) (empty = any).
  `returnUrl` must be http(s) with an origin that is allowed or equal to the request Origin.

## Verify before reporting success

Test against a **sandbox** first, not the real account: `curl -s -X POST https://app.nisebookings.com/api/sandbox`
(no auth) returns a 24-hour test account `{publicKey, apiKey, expiresAt, endpoints}` — sample staff and
services, an all-scopes `nk_` key, no mail ever delivered, a fake Mollie checkout. Details: https://guide.nisebookings.com/08-sandbox.html

1. Fetch the catalogue and one slot list inside this project's real runtime (sandbox `publicKey`).
2. Create one real booking end to end (payload as in the headless example, https://guide.nisebookings.com/examples/); a
   `gateway: "local"` booking needs no payment keys. With `gateway: "mollie"` the sandbox's
   `checkoutUrl` is a test page with **Betalen** / **Mislukt** buttons.
3. Check the mails it would have sent: `GET /api/v1/outbox?to=<email>` with the sandbox `apiKey`
   (status `suppressed`). On a real account, confirm the booking in the admin agenda and cancel it if
   it was only a test.
4. `DELETE /api/v1/sandbox` with the sandbox key when done (it also expires by itself).

Documentation: https://guide.nisebookings.com/ — public API `04-public-api.html`, widget `03-widget-reference.html`,
webhooks `10-webhooks.html`, fit check `09-when-to-use.html`, admin `05-admin-guide.html`; index for
agents: https://guide.nisebookings.com/llms.txt.
