# Quickstart — add bookings to a site

The fastest path: get a public key, paste two lines of HTML, done. Every route below is a live
integration path; pick one.

| You want | Path | Time |
|---|---|---|
| Bookings on a normal website | [Widget](#path-a-widget-on-any-website) | minutes |
| Bookings on a WordPress site | [WordPress](#path-b-wordpress) | minutes |
| Your own UI / headless | [Public API](#path-c-your-own-front-end) | hours |
| Ticket sales for an event | [Events widget](#booking-events-instead-of-appointments) | minutes |
| Just test, no account yet | [Sandbox](08-sandbox.md): `curl -X POST https://app.nisebookings.com/api/sandbox` | seconds |

## Get a key

1. Create an account and complete the onboarding: **Bedrijfsnaam**, **Webadres** (a slug) and
   **Tijdzone**. This creates your account and makes you owner.
2. Add what can be booked: **Medewerkers** (staff/resources, each with a weekly schedule) and
   **Diensten** (services, and which staff perform them).
3. Copy the **Publieke sleutel** from **Instellingen → Insluiten**. That key is the only credential the
   browser-facing side needs.

Optional but recommended before going live: **Instellingen → Bedrijf en e-mail** (sender name,
reply-to), **Instellingen → Betalingen** (VAT, deposit, Mollie key), **Instellingen → Insluiten →
Toegestane websites** (restrict who may embed your widget).

### Let an agent configure your account

Everything above can also be done from code: **Instellingen → API** creates a key with scopes, and the
[management API](../api/README.md) then builds the whole catalogue — categories, services, staff,
per-staff prices, the working week, opening hours and holidays. Nothing to click.

```bash
export API=https://app.nisebookings.com/api/v1 KEY=nk_…
curl -s -H "Authorization: Bearer $KEY" $API/me
curl -s -H "Authorization: Bearer $KEY" -X POST $API/services \
  -d '{"title":"Knippen","duration":30,"price":25,"visibility":"public"}'
```

The quickstart in [`../api/README.md`](../api/README.md) walks an empty account to a real booking, and
the [OpenAPI spec](../api/openapi.yaml) covers every endpoint.

Prefer to ask in plain language? The same key connects your coding agent to the hosted
[MCP server](../api/mcp.md):

```bash
claude mcp add --transport http nise-booking https://app.nisebookings.com/api/mcp \
  --header "Authorization: Bearer $KEY"
```

## Path A: widget on any website

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

That is the whole integration. The element renders the full flow — service, time, details, payment —
inside a shadow DOM, styled with the page's font. Optional attributes:

```html
<nise-booking
  data-key="…"
  data-api="https://app.nisebookings.com"   <!-- needed only when widget.js is hosted elsewhere -->
  data-service="<service id>"              <!-- preselect a service -->
  data-staff="<staff id|any>"              <!-- preselect a staff member -->
  data-lang="nl|en"                        <!-- widget language; default: the account's -->
></nise-booking>
```

Details, styling tokens and limitations: [`03-widget-reference.md`](03-widget-reference.md).

## Path B: WordPress

No plugin needed; this works on any WordPress site where you can add HTML.

1. Edit the page, add a **Custom HTML** block and paste the two lines from Path A (with your public
   key). Optional attributes such as `data-service` and `data-staff` work the same way.
2. Add the WordPress site to **Toegestane websites** in the booking app, or leave the list empty.

Your WordPress user must be allowed to add scripts (administrators are, by default).

## Path C: your own front-end

```js
const KEY = '<your public key>';
const API = 'https://app.nisebookings.com/api/public/' + KEY;

const { tenant, services } = await fetch(`${API}/services`).then((r) => r.json());
// tenant.timeZone, tenant.currency, tenant.gateways, tenant.depositMode, …

const { days } = await fetch(`${API}/slots?service=${serviceId}&from=2026-10-14&days=7&persons=1`)
  .then((r) => r.json());
const slot = days.flatMap((d) => d.slots)[0];

const res = await fetch(`${API}/bookings`, {
  method: 'POST', headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    items: [{ service: serviceId, staff: slot.staffId, start: slot.start, persons: 1 }],
    name: 'Ada Jansen', email: 'ada@example.com', gateway: 'local',
  }),
});
const order = await res.json();          // order.orderToken, order.items[0].status
```

Full endpoint reference, error codes and recipes (Mollie checkout, recurring series, waiting list,
tickets): [`04-public-api.md`](04-public-api.md). Machine-readable: [`openapi.yaml`](openapi.yaml).

## Booking events instead of appointments

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

Publish an event with ticket types in **Evenementen**, then embed. See
[`03-widget-reference.md`](03-widget-reference.md#nise-events).

## Try it first

No account needed: one call creates a [sandbox](08-sandbox.md) — a complete test business with a
public key and an all-scopes management key, valid for 24 hours. It never sends mail (read what it
would have sent via `GET /api/v1/outbox`) and its Mollie checkout is a fake page.

```bash
curl -s -X POST https://app.nisebookings.com/api/sandbox   # → {publicKey, apiKey, expiresAt, endpoints}
```

To just look: `https://app.nisebookings.com/demo.html` (widget + event shop). Runnable code:
[`examples/`](examples/README.md) — `headless.mjs` runs against a sandbox key unchanged.

## Verify your integration

1. Complete a booking through your page. The calendar (**Agenda**) shows it immediately.
2. **Afspraken** shows the booking status; **Betalingen** shows the payment (if priced).
3. **Meldingen → Verzendlog** shows the confirmation mail and its delivery status.
4. The confirmation mail contains a cancel link; open it and cancel to test the customer path.

## Troubleshooting

| Symptom | Cause / fix |
|---|---|
| Widget shows "De agenda kan nu niet worden geladen." | Wrong key, or the API host is unreachable from the browser. Check the network tab: a 404 means the key is wrong. |
| `403 {"error":"origin_not_allowed"}` | The embedding origin is not in **Toegestane websites**. Add it, or clear the list. |
| Widget renders but lists no services | Your account has no services with `visibility = public` and at least one public staff member who performs them. Check **Diensten** and **Medewerkers → Diensten**. |
| No times offered | Staff schedules (or opening hours for new staff), company/staff holidays, minimum notice, booking horizon, and the slot grid. Try another day; check the staff's **Rooster**. |
| Times offered but booking fails with 409 | Someone else took the slot; re-fetch and pick again. |
| Payment link fails / `502 payment_unavailable` | Mollie key missing or invalid, or Mollie temporarily unreachable. Check **Instellingen → Betalingen**. |
| Mails not arriving | Check **Meldingen → Verzendlog** for status and error text; a customer may also find it in their spam folder. |
| Widget shows an older version than expected | Rare — the console shows `nisebookings@<version>` on load; add `?v=<version>` to the script URL (or hard-refresh) if a cache serves an old copy. |

## Going live checklist

- [ ] Real services, staff schedules, holidays (company + per staff) entered.
- [ ] **Instellingen → Algemeen**: time zone, time step, minimum notice, booking horizon.
- [ ] **Instellingen → Afspraken**: new bookings confirmed immediately or pending approval?
- [ ] **Instellingen → Betalingen**: VAT mode, deposit mode, gateways, Mollie live key.
- [ ] **Instellingen → Insluiten**: allowed websites restricted to your domains.
- [ ] Notification templates reviewed in Dutch (sender name, reply-to, texts, reminders).
- [ ] Reminders, payment timeouts and automatic status changes are handled by the service —
      nothing to run yourself.
- [ ] A test booking, cancellation and refund done end to end.
