Passing context
Context pre-positions the booking funnel. The widget opens on the first value that has not been supplied, rather than on the first step.
Transports
Section titled “Transports”Seven values reach the widget through three transports.
<!-- 1. Script tag — for a CMS, where there is no JavaScript to write --><script src="https://app.useservice.app/widget/widget.js" data-slug="chez-marie" data-date="2026-09-03" data-party-size="4" async></script>2. URL — for e-mail, SMS, QR codes, anywhere the host has no JavaScript at all https://book.useservice.app/r/chez-marie?date=2026-09-03&party=4// 3. JavaScript — for context that changes after the page loadswidget.setContext({ date: "2026-09-03", partySize: 4 });Precedence
Section titled “Precedence”data-*, then URL, then setContext(). The last source to supply a value wins.
setContext() ranks highest because context changes after load: a guest may
select dates in the host’s own search interface before opening the widget.
Merging is per field. A source that supplies only partySize does not clear a
date supplied earlier.
The values
Section titled “The values”| Hint | data-* | URL | Format |
|---|---|---|---|
date | data-date | date | YYYY-MM-DD |
time | data-time | time | HH:MM, 24-hour |
partySize | data-party-size | party | Integer, 1–100 |
sectionKey | data-section-key | section | Seating-group key, or none |
timeFrom | data-time-from | from | HH:MM — earliest slot to offer |
timeTo | data-time-to | to | HH:MM — latest slot to offer |
shiftId | data-shift-id | shift | Integer — restricts the funnel to one service |
locale | data-locale | locale | One of the 11 guest locales |
guestNotes | data-guest-notes | notes | Free text, up to 2 000 characters |
The URL names are short and human-typeable on purpose — they end up in printed
QR codes and marketing links, so they deliberately do not mirror the
data-* spelling.
A time window instead of a service
Section titled “A time window instead of a service”timeFrom and timeTo offer only the slots between them, inclusive. They are
the portable alternative to shiftId: a service id is different at every
restaurant, so a group applying one integration to ten sites needs ten lookups,
while 19:00–22:30 means the same thing everywhere.
https://book.useservice.app/r/chez-marie?from=19:00&to=22:30Both are needed. A lone bound is refused rather than read as open-ended — you wrote a range, and half a range silently enforced is worse than none.
They combine with shiftId rather than replacing it, and each narrows
independently: a service and a window means slots that satisfy both.
Finding a section key or a shift id
Section titled “Finding a section key or a shift id”Both are restaurant-specific, and both come from the same unauthenticated call the widget itself makes on open:
curl "https://app.useservice.app/api/v1/public/restaurants/chez-marie?include=section_groups,availability"Section keys are in section_groups. Use key — name is localised and
changes, key does not:
{ "section_groups": { "data": [ { "key": "31", "name": "Terrasse", "area_type": "outdoor", "bookable_online": true }] } }Shift ids are on each day of availability, because a restaurant’s services
differ by day:
{ "availability": { "months": { "2026-09": { "days": [ { "date": "2026-09-03", "shifts": [ { "id": 1, "name": "Déjeuner", "start_time": "12:00", "end_time": "14:30" } ] }] } } } }Read them once and cache them per restaurant; they change only when the restaurant reconfigures its floor plan or its services. There is no separate lookup endpoint — this is the same payload the widget renders from, so a value you can see here is a value the funnel will accept.
Across several restaurants, do not assume the ids line up. “Terrasse” is a
different key at every restaurant, and “Déjeuner” a different shift_id, so a
group applying one integration to ten sites needs one lookup per site.
sectionKey: "none"
Section titled “sectionKey: "none"”sectionKey is the one field whose absence is itself a value. “No seating
preference, and the guest may not change it” is distinct from “no statement
about seating”.
The two are indistinguishable on the wire: after a pruner and a JSON encoder, an
absent key, null and "" are identical. The first meaning therefore travels as
the string "none":
widget.setContext({ sectionKey: "none" }); // no preference, and it is pinnedwidget.setContext({ sectionKey: null }); // says nothing about seatingnull is read as “no statement”, and the seating picker is shown. Sending
null in place of "none" therefore produces the opposite of the intended
result.
"none" is unambiguous as a literal: seating keys are stringified ids, so no
restaurant can have a section of that name.
As a hint, "any" is also accepted (case-insensitive) and means the same
thing as null — “no statement about seating”, picker shown. It exists because
any reads naturally in a URL a human types:
https://book.useservice.app/r/chez-marie?section=anySo the two words are not synonyms, and the difference is the trust tier rather than the seating:
| Value | Where | Meaning |
|---|---|---|
"any" | hint only | No statement — the guest picks, picker shown |
null | hint only | Identical to "any" |
"none" | signed token | No preference, and pinned — picker hidden |
Only a signed token can pin seating; "any" in a URL cannot, because a URL
anyone can edit is not a constraint.
The locales
Section titled “The locales”locale selects the language the funnel mounts in. The widget ships eleven:
| Code | Language | Code | Language | Code | Language |
|---|---|---|---|---|---|
fr | Français | it | Italiano | sv | Svenska |
en | English | nl | Nederlands | no | Norsk |
de | Deutsch | pt | Português | fi | Suomi |
es | Español | da | Dansk |
The match is exact. Region tags and other spellings are not recognised, so
fr-CA, en-GB and FR are all dropped like any other value that does not
parse. Set the code alone.
Each restaurant chooses which of the eleven it offers its guests. The widget shows a language picker when a restaurant offers more than one, and the guest’s choice is kept per restaurant and outranks the value you supply — see deep links.
Supply locale on every surface where you know the guest’s language. It is the
one hint whose absence the guest notices immediately.
Every value on this page is a hint. Hints are forgeable, and the guest can change all of them, so the widget treats them as guest input.
Identity is not a hint and does not travel this way. parseUrlHints has no URL
name for a name, an e-mail or a phone number, so identity fields cannot appear
in a URL by accident. Identity uses a signed token — see
identifying the guest.
A value that does not parse is dropped. It is never partially applied and never
fatal: ?date=nonsense opens the ordinary calendar.
Constraining a value
Section titled “Constraining a value”Hints do not constrain: the widget shows the value and the guest edits it freely. To show it as fixed instead, lock it — from the same three transports:
<script … data-locked="date" data-lock-reason="Gift voucher BC-4471" async></script>A lock set this way is drawn, not enforced. The server accepts a booking
that contradicts it, because a query string or an attribute can be written by
anyone and it has no way to tell yours from theirs. For a value that must
actually hold, put the field in locked inside the signed token, which the
server does enforce.
Both are covered in Locking fields.