Skip to content

Choosing an integration

The widget has four integration surfaces. Which one applies is decided by what the host already has — a page, a page with JavaScript, or only a URL — rather than by preference.

You haveIntegrationContext arrives asIdentityEvents
A page in a CMS, no JavaScriptScript tagdata-*
A page you write JavaScript onJavaScript APIsetContext()setGuestToken()Yes
Only a link — e-mail, SMS, QRDeep linkQuery string?t=
Nothing to integrate intoThe booking page, linked

The first two put the widget on the host’s own page. The last two send the guest to the standalone booking page at book.useservice.app.

A signed token reaches the widget through setGuestToken() or through ?t= on a deep link. There is deliberately no data-token attribute.

An attribute is part of the page’s HTML: it is served to every reader, cached by the CMS and by whatever sits in front of it, and visible in view-source. A token lives 15 minutes and identifies one guest, so a copy baked into a page is both expired and wrong for nearly everyone who receives it.

If the integration must know who is booking, it needs a page that can run setGuestToken(), or a link minted per guest at the moment it is sent.

Enforced locks need a token; drawn ones do not

Section titled “Enforced locks need a token; drawn ones do not”

Any surface can lock a field so the guest sees it as fixed — an attribute, a query parameter or a JavaScript call all do it.

What needs the signed token is enforcement. The server refuses a booking that contradicts a lock only when the lock arrived inside the token, because that is the only version it can trust. An unsigned lock is drawn and not enforced.

That difference decides the surface only when the value must actually hold — a voucher valid on one date, a prepaid slot. For simply reflecting a choice already made on your page, nothing is needed.

reservation:created and reservation:confirmed are delivered to the host page that created the instance. A deep link has no such page: the guest is on book.useservice.app, and the host is not in the browser at all.

Bookings made through a deep link are reported the same way telephone bookings are — through the API or a webhook, on your server. That path also works for the embedded cases, and is the one to use when the record matters: a browser event is not delivered if the guest closes the tab.

inline, sticky and popover decide how the widget appears on a host page. They apply to both embedded surfaces and are independent of everything above: a CMS script tag can be a popover, and a JavaScript integration can be inline.

ModeAppears asSuits
inlinePart of the page, inside your container.A dedicated booking section or page.
stickyA floating button the widget renders itself.A site with no obvious place to put it.
popoverNothing, until a trigger or open() shows it.Your own button, your own styling.

A group site listing eight restaurants creates one instance per restaurant:

const instances = restaurants.map((r) => ServiceWidget.create({ slug: r.slug, mode: "popover" }));

Each instance is addressed on its own. ServiceWidget.open() and ServiceWidget.close() on the global cannot name one among several — close() closes all of them, deliberately, so that no instance can be left open with nothing able to reach it.

With declarative triggers, each trigger carries the restaurant it opens:

<button data-service-widget-open data-slug="chez-marie">Book</button>
<button data-service-widget-open data-slug="le-comptoir">Book</button>
  • A restaurant’s own site, no developer. Script tag, from the back office. It is complete on its own — no further integration is required to take bookings.
  • A group site or a guest portal where the visitor is signed in. JavaScript API with a token: the funnel opens knowing who is booking, and anything the stay already fixes can be locked.
  • A campaign, a booking confirmation, a room-key card. Deep link. Add ?t= only where the link is minted per guest and sent immediately — never on anything printed.