Skip to content

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.

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 loads
widget.setContext({ date: "2026-09-03", partySize: 4 });

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.

Hintdata-*URLFormat
datedata-datedateYYYY-MM-DD
timedata-timetimeHH:MM, 24-hour
partySizedata-party-sizepartyInteger, 1–100
sectionKeydata-section-keysectionSeating-group key, or none
timeFromdata-time-fromfromHH:MM — earliest slot to offer
timeTodata-time-totoHH:MM — latest slot to offer
shiftIddata-shift-idshiftInteger — restricts the funnel to one service
localedata-localelocaleOne of the 11 guest locales
guestNotesdata-guest-notesnotesFree 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.

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:0022:30 means the same thing everywhere.

https://book.useservice.app/r/chez-marie?from=19:00&to=22:30

Both 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.

Both are restaurant-specific, and both come from the same unauthenticated call the widget itself makes on open:

Terminal window
curl "https://app.useservice.app/api/v1/public/restaurants/chez-marie?include=section_groups,availability"

Section keys are in section_groups. Use keyname 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 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 pinned
widget.setContext({ sectionKey: null }); // says nothing about seating

null 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=any

So the two words are not synonyms, and the difference is the trust tier rather than the seating:

ValueWhereMeaning
"any"hint onlyNo statement — the guest picks, picker shown
nullhint onlyIdentical to "any"
"none"signed tokenNo 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.

locale selects the language the funnel mounts in. The widget ships eleven:

CodeLanguageCodeLanguageCodeLanguage
frFrançaisitItalianosvSvenska
enEnglishnlNederlandsnoNorsk
deDeutschptPortuguêsfiSuomi
esEspañoldaDansk

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.

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.