WorkestraDocs
PlatformCalendar & Scheduling

Embed Widget

Drop a Workestra booking calendar straight onto your marketing site — no Calendly subscription, no iframe glue, no extra plan tier.

The embed widget puts a Workestra booking link inside your own site, instead of sending visitors to workestra.app/book/<slug>. Pick a slot, fill the form, confirm — all without leaving your page.

You'd reach for it when:

  • You want a "Book a meeting" button on a pricing page or contact form, not a link that opens a new tab
  • You need the booking experience to feel like part of your brand (your fonts, your background, your domain context)
  • Your marketing analytics need to fire on the booking event (GA4, GTM, Segment) — the widget broadcasts a parent-page event you can listen for

It's free on every plan — same scheduling stack, just with embed affordances turned on.

Inline embed widget on a marketing page

Screenshot needed — embed widget mounted in-page with a date/time picker

Public booking pageEmbed widget
URLworkestra.app/book/<slug>Your own page (e.g. acme.com/contact)
Visitor leaves your site?Yes — opens WorkestraNo — stays on your page
Your branding?Workspace theme onlyYour page chrome around it
Parent-page eventsNoneworkestra:event_scheduled fires on window
SetupCopy the URLCopy a snippet + paste into your HTML

Both routes share the same booking flow under the hood — the same availability rules, pre-meeting form, round-robin pool, and reschedule/cancel tokens. The embed is a stripped-chrome variant of the public page, not a separate product.

Setting it up

Open a booking link at /settings/booking-links/<id> and scroll to the Share & Embed card at the bottom. It has four tabs.

Copy the plain workestra.app/book/<slug> URL to share anywhere — email signature, LinkedIn, outbound sequences.

Embed style tab

Configure how the widget looks and behaves when it's embedded.

SettingWhat it does
Embedding enabledMaster switch. While off, the /embed/<slug> URL shows a "Embedding disabled" message. The direct /book/<slug> page is unaffected.
LayoutInline · Month calendar (full date grid, ~700px tall), Inline · Slot list (compact vertical slot list, good for narrow sidebars), or Popup widget (floating button on the host page that opens a modal).
Hide host info paneHides the left column with host name, duration, and description. Useful when your marketing page already shows that context.
Hide cookie bannerSuppresses the consent banner inside the iframe. Off by default — flip on if your parent page already handles consent.
Skip landing pageJumps straight to the slot list instead of the date picker. Good for tight in-page mounts where space is limited.
LocaleForce a language (EN, FR, DE, etc.) or leave on Auto-detect. Visitors can override with ?locale= in the URL.
Post-booking redirectOptional URL. If set, visitors are sent here after confirming a booking instead of seeing the in-widget confirmation page.

Changes are saved immediately when you toggle or change a field.

Snippet tab

Copy-paste HTML for your developer. Two formats are available depending on the Layout setting in the Style tab.

Inline embed (month calendar or slot list layouts):

<!-- Workestra booking — inline embed -->
<div
  data-workestra-embed-inline
  data-url="https://workestra.app/embed/your-slug"
  style="min-width: 320px; height: 700px;"
></div>
<script src="https://workestra.app/api/scheduling/embed.js" async></script>

Paste it where you want the calendar to appear. The loader auto-resizes the iframe as the flow advances (slot pick → form → confirmation) so the page doesn't jump.

Popup widget layout:

<!-- Workestra booking — popup widget -->
<script src="https://workestra.app/api/scheduling/embed.js" async></script>
<script>
  window.Workestra = window.Workestra || { q: [] };
  Workestra.q.push(["initPopupWidget", {
    url: "https://workestra.app/embed/your-slug",
    text: "Book a meeting",
    color: "#1d4ed8",
    textColor: "#ffffff",
    branding: false,
  }]);
</script>

This injects a floating button in the bottom-right corner of the host page. Clicking it opens a modal with the booking flow. The color and textColor values control the button's background and label color — they're independent of any branding settings inside the widget itself.

The loader (api/scheduling/embed.js) is a small self-mounting script. Drop it on the page once — it scans for data-workestra-embed-inline blocks and processes the command queue for popup mode.

The snippet in the Snippet tab updates live as you change settings in the Embed style tab — if you switch the layout from Inline to Popup, the snippet switches format automatically. Copy it after you've finalized your style settings.

Allowed domains tab

Manage which origins are permitted to iframe the widget. Any domain you add can embed the link; domains you don't add will be blocked by the browser before the widget renders.

Add origins one at a time:

https://acme.com
https://*.acme.com         ← wildcard: accepts blog.acme.com, www.acme.com, etc.
http://localhost:3000      ← for local development

The form rejects anything that isn't http:// or https:// (no javascript:, data:, or file:). Origins are normalized — trailing slashes are dropped, hosts are lowercased, default ports are stripped.

There's also an Allow any origin toggle gated behind a confirmation dialog. Use it only for public docs or demo pages where you genuinely want anyone on the internet to embed. Most teams should leave it off.

Domain enforcement is in progress. The allowed-origins list is stored and editable today, but browser-level CSP enforcement (which actually blocks unauthorized iframes) is being added in a follow-up release. Until then, the list is a configuration record — the widget will load from any origin regardless of what's listed.

Pre-filling and UTM passthrough

The widget accepts attendee details as part of the snippet, so the booking flow opens with the form already filled in.

Inline embed:

<div
  data-workestra-embed-inline
  data-url="https://workestra.app/embed/your-slug"
  data-prefill='{"name":"Alice Smith","email":"alice@acme.com","a1":"Acme Corp"}'
  style="min-width: 320px; height: 700px;"
></div>
<script src="https://workestra.app/api/scheduling/embed.js" async></script>

Popup widget:

<script>
  Workestra.q.push(["initPopupWidget", {
    url: "https://workestra.app/embed/your-slug",
    text: "Book a meeting",
    prefill: { name: "Alice Smith", email: "alice@acme.com", a1: "Acme Corp" },
    utm: { utm_source: "homepage_hero", utm_medium: "embed" },
  }]);
</script>

UTM parameters from the parent page URL (utm_source, utm_medium, utm_campaign, utm_content, utm_term) are auto-captured and passed through to the booking. They land on the booking row, are available as {{utm_*}} variables in Workflows, and are appended to any post-booking redirect URL.

Multi-CTA confirmation

The confirmation page inside the embed can show up to 5 call-to-action buttons (configured per booking link — see Booking Links). Useful for retaining attention after the booking — "Visit pricing", "Download our deck", "Slack our team", etc.

When a booking link has paid bookings enabled, the embed flow hands off to Stripe Checkout via top-level navigation (the checkout page can't render inside an iframe — Stripe blocks it). The host page is replaced with the Stripe Checkout page, and on success the visitor returns to the in-widget confirmation. No iframe gymnastics required on your side.

Polls, one-off, and single-use embeds

Beyond standard booking links, you can embed:

Same loader, different data-url (/embed/poll/<token>, /embed/one-off/<token>, /embed/single-use/<token>).

CLS and reserved height

The inline embed reserves its height before the iframe loads (style="height: 700px" on the container). This means Cumulative Layout Shift = 0 on your Lighthouse scorecard for that block. If you set height: auto, the loader will try to grow the iframe as content changes — but the initial render shifts. We recommend the fixed-height pattern for performance-critical pages.

GDPR — single-origin

Workestra serves the embed and all its asset requests from a single origin: workestra.app. No additional third-party domains are touched. (For comparison, Calendly's embed loads from 7 distinct origins including assets.calendly.com, cdn.segment.com, etc., each requiring its own consent line.)

The single-origin design means your cookie consent banner only needs one Workestra entry, and your CSP connect-src only needs one origin. The first-party / third-party split: cookies set on workestra.app are first-party to Workestra, third-party to your site.

NPM packages — React, Angular, React Native

For SPAs and mobile apps where pasting an HTML snippet isn't ergonomic, three packages are available:

PackageUse
@workestra/react-scheduling-embedReact component — <WorkestraEmbed url="..." prefill={...} />. Tree-shaken, no script tag injection.
@workestra/angular-scheduling-embedAngular module — <workestra-embed [url]="..." [prefill]="..."></workestra-embed>.
@workestra/scheduling-mobile-sdkReact Native SDK — wraps the embed in a WebView with native event bridging for onScheduled.

Source for all three lives in apps/ of the Workestra repo. Install via npm; no API key required.

Listening for events on your page

When a visitor confirms a booking, the widget emits a CustomEvent on the parent window. Wire any tag manager or analytics tool to it:

window.addEventListener("workestra:event_scheduled", (e) => {
  const { booking_id, attendee, starts_at } = e.detail;
  gtag("event", "meeting_booked", {
    booking_id,
    email: attendee.email,
    starts_at,
  });
});

Other events:

EventFires when
workestra:event_type_viewedThe widget finishes loading and the visitor sees the calendar
workestra:date_selectedThe visitor picks a date
workestra:event_scheduledA booking is confirmed — the most common one to wire
workestra:embed_heightThe iframe auto-resizes — the loader handles this; you usually don't need to listen

The event.detail payload for event_scheduled includes the booking ID, slug, start/end times, attendee fields (name, email, phone if supplied), and any UTM params present on the parent URL.

Privacy & data

The embed page lives on workestra.app, so cookies set there are first-party to Workestra and third-party to your site. The booking confirmation event fires the attendee's name and email back to your parent page so your CRM and analytics can pick them up — that's intentional, but it's your privacy policy's responsibility to disclose that you're collecting this data via the widget.

Workestra does not pass any other workspace data, any other bookings, or any other attendees — only the booking that just happened.

Developer quick-start prompt

If you're handing the integration off to a developer, give them this:

We're embedding a Workestra booking calendar on our site. Copy the HTML snippet from the Share & Embed → Snippet tab on the booking link settings page. Paste it into the page where you want the calendar to appear. The snippet includes a <script> tag that loads the Workestra embed loader — add it once per page, not once per widget. The inline embed needs a container with a fixed height (start at 700px); the loader will auto-resize it as the user moves through the flow. For the popup widget, the script injects a floating button automatically — no container needed. No build step, no npm package, no API key required.

  • Booking Links — set up the underlying link before you embed it
  • Round-Robin — round-robin links work transparently inside the embed
  • Pre-Meeting Forms — form fields work inside the embed exactly the same way
  • Calendar Dashboard — embedded bookings show up on your /calendar like any other