# Widget
Paste one script tag where bookings should appear: ``. Your booking flow renders in place, or behind a floating Book now button with `data-mode="button"`, and your page hears about each booking that confirms there through a `daybooked:booked` event (paid ones confirm after Stripe Checkout, on the `booking.confirmed` webhook).
URL: https://daybooked.io/docs/widget
## Install
Paste it where bookings should appear. The flow renders right after the tag, or inside `data-target`.
```html
```
## Button
A Book now pill in the bottom-right corner opens the flow in a dialog; Escape or a click outside closes it. On a dark site, `data-theme="light"` makes it white, or `"auto"` picks by what's behind it; `data-position="left"` and `data-offset` move it clear of a chat bubble or a cookie banner.
```html
```
## Options
- data-org (e.g. maple-street): Your booking page slug. Required.
- data-offering (e.g. 8f1c2e4a-0b7d-4c55-9a7e-3f2d1c0b9a88): Open straight to one offering.
- data-mode (e.g. button): inline (default) renders in place; button floats a pill that opens it.
- data-label (e.g. Book a class): The button's text. Default "Book now".
- data-target (e.g. #book): CSS selector to render into, instead of next to the tag.
- data-theme (e.g. light): The button's look: dark (default), light (a white pill for dark sites), or auto, which picks by what's behind it as the page scrolls.
- data-position (e.g. left): The button's corner: right (default), or left to keep clear of a chat bubble.
- data-offset (e.g. 24 96): The button's distance from its corner in px: one number, or the side and the bottom (default 20). Lift it above a cookie banner.
- data-analytics (e.g. datalayer fbq): Your page's tags that also get each event: datalayer, gtag, fbq (see Analytics). Off by default.
## Events
Your page hears what a guest does as window events. Each `event.detail` has `org` and the booking's facts, never the guest's name, email or phone: `value` in major units (190 for $190), `currency` as an ISO code, `offering` as `{ id, name, kind }`, `starts_at` in UTC (`2026-12-05T15:00:00Z`).
- daybooked:view: An offering is shown or chosen: `offering`, with `value` at its price for one.
- daybooked:begin: The guest reaches their details, once for each time and party they pick: `value` for them, `quantity`, `starts_at`.
- daybooked:checkout: The place is held and your page is about to go to Stripe Checkout: the `reference` too.
- daybooked:booked: A booking confirms in the widget: `reference`, `value`, `currency`, `offering`, `quantity`, `starts_at`. One paid through Stripe Checkout doesn't fire it: see Paid bookings.
```js
window.addEventListener("daybooked:booked", (event) => {
const { reference, value, currency, offering } = event.detail; // e.g. "0GWYRZ2V", 190, "USD", { id, name, kind }
});
```
## Analytics
Tags on your page can't see inside the widget's frame, so the widget hands them each event, in their own names. Name your tags in `data-analytics`; it's off until you do.
- datalayer: Pushes GA4 ecommerce events to `window.dataLayer` for Google Tag Manager: `view_item`, `begin_checkout` at the details, `purchase` when booked.
- gtag: Calls `gtag("event", …)` with the same, on a site with the Google tag and no Tag Manager. Pick `datalayer` or `gtag`, not both, or GA4 hears each event twice.
- fbq: Tracks the Meta Pixel's `InitiateCheckout` at the details and `Purchase` when booked, with `value` and `currency`, and the `reference` as Purchase's `eventID`.
```html
```
### Google Tag Manager
1. Add `data-analytics="datalayer"` to the widget's tag.
2. In Tag Manager, add a tag of type Google Analytics: GA4 Event, with your measurement ID and `{{Event}}` as its Event Name.
3. Under More Settings → Ecommerce, tick Send Ecommerce data, with Data Layer as its source.
4. Fire it on a Custom Event trigger named `^(view_item|begin_checkout|purchase)$`, with Use regex matching.
5. Preview, make a test booking on your page (cancel it in daybooked after), then Submit.
### Meta Pixel
With the Meta Pixel on your page, add `fbq`: `data-analytics="datalayer fbq"` feeds both.
### Paid bookings
A paid booking confirms after Stripe Checkout, once the guest has left your page, so `daybooked:booked` never fires for it. Send it from your server when its `booking.confirmed` webhook arrives with a `payment_intent_id`; the bookings your page heard about have none, so nothing counts twice.
```js
// Your endpoint for daybooked's webhooks, as Node's http.createServer(onWebhook). daybooked waits 5 seconds
// for the answer, so check the signature (verify() from /docs/webhooks), answer, and only then send.
async function onWebhook(req, res) {
let body = "";
for await (const chunk of req) body += chunk;
if (!verify(process.env.DAYBOOKED_WEBHOOK_SECRET, body, req.headers["daybooked-signature"] ?? "")) {
return res.writeHead(401).end();
}
res.writeHead(204).end();
const event = JSON.parse(body);
// The sends you use. On a serverless host, hand them to its waitUntil rather than awaiting them here.
await Promise.allSettled([sendToGA4(event), sendToMeta(event)]);
}
```
```js
async function sendToGA4(event) {
const booking = event.data;
if (event.type !== "booking.confirmed" || !booking.payment_intent_id) return;
const kinds = { class: "Class", appointment: "Private", rental: "Rental" };
const value = booking.total_cents / 100; // minor units to major: leave out "/ 100" for a zero-decimal currency such as JPY
const url = `https://www.google-analytics.com/mp/collect?measurement_id=${GA_MEASUREMENT_ID}&api_secret=${GA_API_SECRET}`;
await fetch(url, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
// A stand-in, so GA4 records the sale without the visit's source: send the visitor's own client ID if your page saved it.
client_id: booking.id,
timestamp_micros: Date.parse(event.created_at) * 1000,
events: [{
name: "purchase",
params: {
transaction_id: booking.reference,
value,
currency: "USD", // your org's currency
items: [{
item_id: booking.offering.id,
item_name: booking.offering.name,
item_category: kinds[booking.offering.kind],
quantity: booking.quantity,
price: value / booking.quantity,
}],
},
}],
}),
});
}
```
```js
import { createHash } from "node:crypto";
async function sendToMeta(event) {
const booking = event.data;
if (event.type !== "booking.confirmed" || !booking.payment_intent_id) return;
// Meta matches a sale to a person by their email: trimmed, lower case, SHA-256.
const email = createHash("sha256").update(booking.customer.email.trim().toLowerCase()).digest("hex");
await fetch(`https://graph.facebook.com/v25.0/${PIXEL_ID}/events?access_token=${META_ACCESS_TOKEN}`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
data: [{
event_name: "Purchase",
event_time: Math.floor(Date.parse(event.created_at) / 1000),
event_id: booking.reference, // the eventID of the Pixel's own Purchase, so Meta counts one
action_source: "other", // "website" also needs the guest's browser user agent, which a webhook lacks
user_data: { em: [email] },
custom_data: { value: booking.total_cents / 100, currency: "USD" }, // your org's currency; JPY and other zero-decimal ones: no "/ 100"
}],
}),
});
}
```
## Payments
When guests pay at booking, the widget holds their place for 35 minutes and sends the page to Stripe Checkout. Paying confirms it, shows the guest a confirmation and emails them the link to their booking page. Follow those with `booking.confirmed` webhooks.
## Site builders
Step-by-step guides for the website builders businesses use.
- Squarespace (Code block): https://daybooked.io/guides/squarespace
- Webflow (Code Embed): https://daybooked.io/guides/webflow
- WordPress (Custom HTML block): https://daybooked.io/guides/wordpress
- Wix (Embed HTML): https://daybooked.io/guides/wix
- Shopify (Custom Liquid section): https://daybooked.io/guides/shopify
- Framer (Embed): https://daybooked.io/guides/framer
- Google Sites (Embed code): https://daybooked.io/guides/google-sites