Skip to content

Booking widget

The widget can be integrated in two ways:

  • Script tag — a single HTML snippet that renders the booking form. No JavaScript is required. This is the snippet provided in the restaurant’s back office.
  • JavaScript API — the same widget, created and controlled from your own code. Use this when the page needs to set context, respond to events, or manage the widget’s lifecycle.

Add the snippet where the booking form should appear:

<div id="service-widget"></div>
<script
src="https://app.useservice.app/widget/widget.js"
data-slug="chez-marie"
data-mode="inline"
async
></script>

data-slug identifies the restaurant. The back office shows the snippet with the correct slug under Settings → Widget.

The widget loads the restaurant’s availability, takes the booking, and confirms it. No further integration is required.

AttributeValuesDescription
data-slugRestaurant slugRequired. Identifies the restaurant.
data-modeinline, sticky, popoverPresentation mode. Defaults to inline.
data-positionbottom-right, top-left, and so onAnchor for the floating trigger. sticky only.

Presentation modes:

ModeBehaviour
inlineRenders in the flow of the page, inside #service-widget.
stickyRenders a floating Reserve button that opens the form.
popoverRenders nothing until opened, either by a trigger element or by the JavaScript API.

data-mode="manual" was the earlier name for popover. It was removed on 6 August 2026: it now falls back to inline like any other unrecognised value. The back office emits popover, which is also the name the JavaScript API uses.

In popover mode, any element with the data-service-widget-open attribute opens the widget:

<button data-service-widget-open>Book a table</button>

The element is styled by the host page. No JavaScript is required.

https://app.useservice.app/widget/widget.js is a stable address that redirects to a content-hashed file. The hashed file is cached for a year and never changes; the redirect is cached for five minutes, which is how long a release takes to reach pages that already have the widget on them.

Load it from that address. A copy served from your own domain, a copy compiled into your bundle, and a CMS cache plugin that rewrites the URL all pin the version you took and stop receiving fixes.

async is supported and is what the back-office snippet emits.

The data-* attributes are read from the script tag that loaded the bundle, so they belong on that tag and not on another one.

The script installs window.ServiceWidget while it executes, and mounts the script tag’s own widget once the document has been parsed. With async, the point at which it executes is not ordered against your inline scripts, so code calling ServiceWidget.create() runs from the load event — as in the example below — or from the script tag’s own onload.

ServiceWidget.version reports the bundle version. One version is published at a time; there is no version to pin and no upgrade to schedule.

The script tag installs window.ServiceWidget. ServiceWidget.create() returns an instance:

<script src="https://app.useservice.app/widget/widget.js" async></script>
<script>
window.addEventListener("load", async () => {
const widget = ServiceWidget.create({ slug: "chez-marie", mode: "popover" });
await widget.ready;
widget.on("reservation:confirmed", ({ reservation }) => {
console.log("booked", reservation.publicId);
});
document.querySelector("#book").addEventListener("click", () => widget.open());
});
</script>
OptionTypeDescription
slugstringRequired. Identifies the restaurant.
modeinline | popover | stickyPresentation mode. Defaults to inline.
containerstring | HTMLElementMount point for inline. Defaults to #service-widget.
localestringInitial guest locale. Invalid values fall back to fr.
positionstringAnchor for the floating trigger. sticky only.

Each call to create() returns an independent instance. open(), close(), setContext(), setGuestToken() and on() are methods on that instance, not on ServiceWidget.

A page may carry several instances — a group site listing several restaurants — and the global object cannot address one of them individually.

widget.ready resolves when the instance has mounted. Methods called before that point are honoured rather than dropped, so awaiting it is optional. It also resolves if the instance is destroyed before mounting, so it never hangs.

create() throws synchronously when mode: "inline" and its container cannot be resolved. An incorrect selector surfaces at the call site rather than as a form that does not appear.

on() returns an unsubscribe function. Call it when the subscribing component is removed.

widget.on("reservation:created", ({ reservation }) => {});
widget.on("reservation:confirmed", ({ reservation }) => {});

reservation:created fires for every booking that reaches an outcome, including bookings awaiting restaurant approval and bookings holding a table pending a card guarantee. reservation:confirmed fires only when the guest holds a confirmed table.

Sending a confirmation message on reservation:created will eventually notify a guest who has an unpaid hold rather than a table.

The event payload contains publicId and no contact details. publicId is the join key for the API and for webhooks, both of which run server-side.

The full catalog is in Events.

The widget writes to the restaurant’s calendar directly. There is no step in which the host receives a booking and forwards it, and no API key is involved: checking availability and booking are public operations against a single restaurant.

To receive bookings in other systems, read them through the API or subscribe to webhooks. This is the same path a telephone booking takes.