Widget reference
Two dependency-free custom elements turn any HTML page into a booking page:
<nise-booking>— the booking flow (service → time → details → payment → done).<nise-events>— the event/ticket shop.
Both render into an open shadow DOM, read their configuration from data-* attributes, and talk to
the public API directly from the browser. There is no iframe, no build step and no
npm package.
Contents
- Embedding
- Language
- The API base URL
<nise-booking>attributes- Booking flow step by step
- Package mode
- Recurrence, cart and chain in the widget
- Waitlist and occupancy
- Payment step and return handling
<nise-events>- Styling
- Accessibility
- WordPress
- Limits, caching and versioning
- What the widget cannot do
Embedding
<script src="https://app.nisebookings.com/widget.js" defer></script>
<nise-booking data-key="YOUR_PUBLIC_KEY"></nise-booking>
<script src="https://app.nisebookings.com/events.js" defer></script>
<nise-events data-key="YOUR_PUBLIC_KEY"></nise-events>
- The scripts are classic scripts (not modules), need no dependencies, and register the element only if it is not registered yet — loading the script twice is harmless.
- Keep
defer: the elements upgrade themselves and start loading as soon as they are connected. - One element instance per page slot. The element has no JS API: no constructor, no methods, no events
(details below); the only global it sets is
window.nisebookings.version. - The widget needs no cookies and sends no credentials.
Language
The customer-facing texts ship in Dutch and English. Each element resolves its language on its own, in this order:
data-langon the element — trimmed and lowercased; anything starting withnlis Dutch, everything else English. Use it to run one page, or one embed, in another language than the account.- The account's language — Instellingen → Algemeen → Taal van het boekingsformulier
(Nederlands/Engels;
localeinPATCH /api/v1/settings). It arrives withGET services. - Dutch, when neither is set.
<nise-booking data-key="…" data-lang="en"> sets lang on its host element (the shadow tree
inherits it) and re-renders the current step when data-lang changes; <nise-events data-lang="en">
reads the attribute on connect and again after the first fetch. On WordPress the plugin shortcodes
take the same value: [nise_booking lang="en"] and [nise_events lang="en"] ("nl" for Dutch);
without it they follow the account language.
Not translated on purpose: server data (service, staff and event titles and descriptions) and
numbers and dates. Formatted values follow the account's locale, so an English widget on a Dutch
account shows Dutch month names and € 1.234,56 — set the account language as well if you want
everything in English.
The API base URL
The widget decides once, at script-load time, where the API lives:
data-api="https://app.nisebookings.com"if present — used verbatim (no trailing-slash normalisation);- otherwise the origin of the script's own
src(https://app.nisebookings.com/widget.js→https://app.nisebookings.com); - otherwise the embedding page's origin.
If you load the scripts from anywhere other than https://app.nisebookings.com, data-api is
mandatory. Note that document.currentScript is
null for module scripts, inline scripts and dynamically injected async scripts; in those cases the
fallback is the page origin, so set data-api explicitly.
Requests without an Origin header work too (server-to-server), but for browser embeddings your
account's Toegestane websites list applies (Instellingen → Insluiten). Empty list = any site;
otherwise the API answers 403 origin_not_allowed.
<nise-booking> attributes
| Attribute | Default | Meaning |
|---|---|---|
data-key |
— | Required. Your account's public key. |
data-api |
script origin | API base URL (see above). |
data-service |
— | Preselect a service id (from GET services). The widget skips the service step only when the service has fewer than 2 staff (or hides staff), no extras, capacity 1, the chain feature is off and the entry is not already in the cart. |
data-staff |
any |
Preselect a staff id, or any. Ignored when the chosen service is not offered by that staff member (falls back to any). |
data-package |
— | Package mode for an already bought package: the token from the /p/<token> link. |
data-lang |
account language | Language of the customer-facing texts: nl or en (anything not starting with nl means English). See Language. |
Attributes are read on connect only — except data-lang, which the element observes: changing it
after the element is in the DOM switches the language (and the host's lang attribute) and
re-renders. No other attribute has an attributeChangedCallback, and re-inserting the same element
does not re-initialise it. If you need a different configuration, create a new element (or replace
the old one).
Booking flow step by step
Steps are 0 services, 1 time, 2 details/payment, 3 done, 4 payment-status message,
5 cart.
Step 0 — Kies een dienst. Service list with title, description, duration and price; packages show
"N dagen geldig"/"onbeperkt geldig". A staff <select> ("Medewerker", first option "Maakt niet uit")
appears only when the service has more than one public staff member and the business has not hidden
staff for collaborative services. "Aantal personen" appears when capacity > 1. Extras render as
checkboxes (single-choice) or number inputs; mandatory extras are pre-set and disabled. Buttons:
Kies een tijd (packages: Verder), Dienst toevoegen (chain), Deze dienst weglaten /
Geen dienst toevoegen, Terug naar je afspraken (cart). Empty: "Er zijn nu geen diensten te
boeken."
Step 1 — Kies een tijd. One week at a time with Vorige week / Volgende week
(hasMore from the API decides whether "next" is enabled). Slots are grouped per day with
localised headings. Occupancy is shown only when Bezetting tonen is on and capacity > 1: 3/8
("Bezette plaatsen") or nog 5 ("Vrije plaatsen"). Waiting-list slots are greyed with
"wachtlijst (N)". Loading text "Tijden laden…", empty "Geen vrije tijden in deze week. Kijk een week
verder.", failure "De tijden kunnen nu niet worden geladen. Probeer het opnieuw."
Step 5 — Je afspraken (cart; shown when winkelmand is on or the entry is recurring). Lists each entry with service, extras, day/time, staff, "· herhaald" for series, and per entry Wijzigen, Verwijderen and, when the service allows repeats, Herhalen / Herhaling aanpassen. Footer: Verder and Nog een afspraak.
Step 2 — Je gegevens. Fields Naam, E-mail, Telefoon, Opmerking, then the service's own intake
questions (Instellingen → Diensten → Velden: text, textarea, choice, checkbox, number, date or file —
JPG/PNG/WebP/PDF up to 5 MB, uploaded before the booking is sent; a required one is marked, a
per-person one renders a control per person as "Persoon 1", "Persoon 2", and a conditional one
(Toon alleen als) appears and disappears as the customer answers the question it depends on —
see the public API's
fields and {{fields}} in mails); then, when something has to be
paid: the coupon field (Toepassen), payment method radios (Ter plaatse betalen / Online
betalen (iDEAL, creditcard en meer) — only when the business offers both), and, for Mollie orders where
any service has a deposit other than 100%, the choice Alleen de aanbetaling / Het hele bedrag.
A live price summary (refreshed through POST quote, 250 ms debounce) shows Totaal, plus Subtotaal
and Korting only when a coupon applies, a VAT line ("waarvan btw" when prices include VAT, "Btw"
otherwise) only when VAT is non-zero, "Nu te betalen" / "Later te betalen" for deposits and "Ter
plaatse te betalen (herhaalde afspraken)" when part of a series is paid on site.
Submit: Afspraak bevestigen, or Naar betalen for an online payment. Notes appear for waiting
list ("Je komt op de wachtlijst, je betaalt nu niets.") and package bookings ("Gebruik het
e-mailadres waarmee je het pakket hebt gekocht.").
Step 3 — done. Heading and text follow the result: "Je afspraak staat vast" when every timed item
is approved, "Je aanvraag is ontvangen" when something is still pending, "Je staat op de wachtlijst"
for waiting-list-only orders, "Je pakket is besteld" for package purchases.
Nog een afspraak maken restarts (in package mode it reloads the package).
Error handling is inline and specific:
| Situation | Message |
|---|---|
| Slot taken between choosing and submitting (409) | "Een gekozen tijdstip is net geboekt. Kies een ander tijdstip." — the entry is highlighted, or reopened for editing when it was the only one. |
| Validation (422) | Per-field messages ("Vul je naam in.", "Vul een geldig e-mailadres in.", "Vul een geldig telefoonnummer in.", "Je opmerking is te lang.") + "Controleer de gemarkeerde velden."; focus moves to the first invalid field. |
| Invalid coupon | "Deze code is ongeldig of al gebruikt." / "Deze code is verlopen." — the field is cleared and the price re-quoted. |
| Package errors | "Dit pakket hoort bij een ander e-mailadres.", "Dit pakket is verlopen.", "Dit pakket is opgebruikt." |
| Waiting list full | "De wachtlijst voor dit tijdstip is vol. Kies een ander tijdstip." |
| Mollie unavailable (502) | "Online betalen lukt nu even niet. Kies ter plaatse betalen of probeer het later opnieuw." |
| Anything else | "Er ging iets mis bij het boeken. Probeer het opnieuw." |
| Services unreachable | "De agenda kan nu niet worden geladen. Probeer het later opnieuw." |
Package mode
<nise-booking data-key="…" data-package="<token>"> (this is what /p/<token> renders) loads
GET packages/<token> and locks the widget to that package:
- The service list is replaced by the package's service with the package's fixed staff (or free choice), price 0, and the widget jumps straight to the time step.
- A banner shows "
<packageTitle>· nogNvanM· geldig t/mdate". - Cart and chain are disabled inside package mode; the API refuses paid booking of package visits.
- Invalid or used-up packages render "Dit pakket is opgebruikt of verlopen." / "Dit pakket is niet gevonden."
Recurrence, cart and chain in the widget
- Herhalen (visible per entry when the service offers frequencies): choose Hoe vaak (Elke dag /
Elke week / Om de week / Elke maand), the interval for daily ("Om de hoeveel dagen"), weekdays for
weekly ("Op", ma–zo), a monthly option (a day number, "de Ne
", or "de laatste "), and either Aantal keer (2–52) or Of tot en met (a date). Data zoeken calls POST recurrenceand lists every occurrence; moved ones show "· ander tijdstip", ones without a time show "· geen tijd gevonden", each removable. The whole series is sent as one order with a series key. - Cart: with winkelmand on, selections accumulate and are sent as one order; earlier entries are
passed as
holdduring slot searches so the customer never sees their own choices as free. - Chain: "Dienst toevoegen" adds a second service that must run right after the first; the time
step then searches chain slots (
POST slotswith multiple items) and shows each part's time.
Waitlist and occupancy
Waiting-list slots appear only when the business enabled the waiting list and the service allows it. A
slot row shows "wachtlijst (N)" where N is the number of people already waiting. Booking one creates a
waitlisted booking without payment; when a seat frees up the customer receives the waitlist_offer
mail with a first-come-first-served /w/<token> link.
Payment step and return handling
For an online (Mollie) payment the widget redirects to the returned checkoutUrl. On return, with
nb_pay=<token> in the URL, it removes that parameter (history.replaceState), shows "Betaling
controleren…" and polls GET payments/<token> every 2 seconds, up to 15 times:
- completed → the done step ("Je betaling is ontvangen.").
- failed/refused → "Betaling niet gelukt" / "De betaling is niet gelukt. Je afspraak is niet geboekt." with Opnieuw proberen (restarts the flow at the service step and clears the cart).
- still pending after 30 s → "Betaling wordt verwerkt" / "Je betaling wordt nog verwerkt. Je krijgt een bevestiging per e-mail."
You do not need a thank-you page of your own when you use the widget: pass a returnUrl on your page
and the widget handles the return. When you build a custom checkout, do the same polling yourself —
see 04-public-api.md.
<nise-events>
Attributes: data-key, data-api and data-lang (read on connect and again after the first fetch;
see Language).
- List: each published event shows title, date range (localised, business time zone), optional image,
info, and a row per ticket type with price ("gratis" when 0) and "nog N" or "uitverkocht"; quantity
inputs are capped at
min(left, 50). Tickets kiezen continues; without a selection: "Kies ten minste één ticket." - Form: Naam, E-mail, Telefoon, and when the total is > 0 and both gateways are enabled, the same
Betaalwijze radios. Tickets bestellen sends
POST events/<id>/tickets. - Done: "Je tickets zijn besteld" with the codes, or "Je codes krijg je na betaling." when the order is not approved yet.
- Payment return: the same 2-second polling, but it requires both
nb_payandnein the URL (ne=1is what this widget writes into its ownreturnUrl, so a booking widget on the same page does not pick up the ticket payment). - No coupon, no notes, no deposit choice, no per-item price breakdown.
- Errors: "Er zijn niet genoeg tickets meer. Kies opnieuw." (sold out), "De verkoop voor dit evenement is gesloten.", "Online betalen lukt nu even niet. Kies ter plaatse betalen of probeer het later opnieuw.", "Er ging iets mis bij het bestellen. Probeer het opnieuw."
Styling
The shadow DOM isolates internal styles; the host exposes design tokens as CSS custom properties. Set them on the element (or any ancestor):
nise-booking, nise-events {
--nb-accent: #E5006F; /* buttons, links, highlights */
--nb-accent-hover: #C2005E;
--nb-text: #1A1A1F;
--nb-text-2: #4A4B68;
--nb-text-3: #6E6F88; /* muted meta text */
--nb-bg: #fff;
--nb-sunken: #F7F8FB; /* cards, panels */
--nb-border: #E6E7EE;
--nb-border-strong: #C7C6D1;
--nb-danger: #CF2E36;
--nb-focus: #FF4D9B; /* focus ring */
--nb-radius: 4px;
}
fontis inherited from the page (the host sets onlyfont-weight: 300andline-height: 1.6), so your typography reaches the widget; sizes and colours do not.- The host is
display: block; max-width: 640px. Control the width with a wrapper. - There are no
::parthooks and no class API — the internal class names are implementation detail. Anything not listed above cannot be restyled. - Customer-facing texts are the built-in Dutch/English strings (Language); only names,
prices, durations and date formatting come from the API (your account's
locale,currencyandtimeZone).
Accessibility
- The error line is
role="alert" aria-live="assertive"; the loading line and the price summary arearia-live="polite". - Headings are
h2withtabindex="-1"and receive focus on each step change. - Service choices are buttons with
aria-pressed; the selected one is exposed as a toggle state. - Invalid fields get
aria-invalidplusaria-describedbypointing at their message, and focus moves to the first invalid field after a failed submit. - Inputs are wrapped in or bound to
<label>s; focus styling uses:focus-visiblewith a 2 px ring. - The container is keyboard-operable end to end (links, buttons, radios, selects).
WordPress
No plugin is needed. In the block editor, add a Custom HTML block to any page or post and paste the
two lines from Embedding (widget.js + <nise-booking>, or events.js +
<nise-events>). The data-service, data-staff and other attributes work the same way,
data-lang included. The plugin's shortcodes take the language as lang: [nise_booking lang="en"]
and [nise_events lang="en"] (or "nl"; without it, the account's language). Your
WordPress user must be allowed to add scripts (administrators are, by default). Add the
WordPress site to Toegestane websites in the booking app, or leave that list empty.
Limits, caching and versioning
| Limit | Value |
|---|---|
| Items per order | 60 (server), chain parts 10 (server); the widget itself adds no cap |
| Chain/search window | 31 days per slots call (the widget asks one week at a time) |
| Recurrence | 2–52 occurrences, interval 1–30 |
| Name / email | 255 characters |
| Phone | 50 characters |
| Notes | 2000 characters |
| Coupon code | the widget's field allows 50; the API accepts 40 |
| Ticket quantity | 1–50 per type, at most 20 types |
| Script sizes (budget) | widget.js ≤ 20 KB gzip (≈16.5 KB now), events.js ≤ 8 KB gzip (≈5.8 KB now); both languages ship in the same file |
- Version marker. The first of the two scripts that loads logs
nisebookings@0.4.0to the browser console (once per page) and setswindow.nisebookings.version. That is how you tell which build a page is running — a page that mixes a stale cached script with a fresh one reports whichever loaded first. - Cache-busting. Appending a version query to the script URL (
/widget.js?v=0.4.0) is supported: it forces a fresh fetch, and the file served does not depend on the query. The WordPress plugin adds?ver=0.4.0automatically. After an update a browser may otherwise briefly keep an old copy; a hard refresh fixes it too. - Load both scripts from
https://app.nisebookings.comso you always get the current version. A copy served from elsewhere needsdata-apiand does not update itself.
What the widget cannot do
- Intake questions and file uploads (JPG, PNG, WebP or PDF, up to 5 MB) yes, both in step 2; customer accounts no.
- Only Dutch and English (Language): no third language, no way to override individual strings.
- No JS API: no events fired, no methods to call, no way to drive it from your own script. If you need that, build on the public API instead.
- No rescheduling inside the widget; customers cancel or move their booking through the mail link
(
/b/<token>). - Day-based services (rentals per whole day,
durationDays≥ 1) show a start-date picker instead of the time grid: the service line reads "N dagen · € X per dag", the customer picks a start date, and the summary and confirmation show the span ("woensdag 21 oktober → zaterdag 24 oktober · 3 dagen"). Such a service cannot be combined with other items in a chain or cart (button hidden, the API also refuses it). - No styling beyond the custom properties above, no
::part, no theme attribute.