Embed Widget

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

The embed widget puts a Leenops booking link inside your own site, instead of sending visitors to https://workspace.leenops.com/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.

Public booking pageEmbed widget
URLhttps://workspace.leenops.com/book/<slug>Your own page (e.g. acme.com/contact)
Visitor leaves your site?Yes — opens LeenopsNo — stays on your page
Your branding?Workspace theme onlyYour page chrome around it
Parent-page eventsNoneleenops: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 https://workspace.leenops.com/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. The /book/<slug> page detects iframe context. While embedding is off, the iframe shows an "Embedding disabled" message; the direct 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):

<!-- Leenops booking — inline embed -->
<div
  data-leenops-embed-inline
  data-url="https://workspace.leenops.com/book/your-slug"
  style="min-width: 320px; height: 700px;"
></div>
<script src="https://workspace.leenops.com/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:

<!-- Leenops booking — popup widget -->
<script src="https://workspace.leenops.com/api/scheduling/embed.js" async></script>
<script>
  window.Leenops = window.Leenops || { q: [] };
  Leenops.q.push(["initPopupWidget", {
    url: "https://workspace.leenops.com/book/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-leenops-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-leenops-embed-inline
  data-url="https://workspace.leenops.com/book/your-slug"
  data-prefill='{"name":"Alice Smith","email":"alice@acme.com","a1":"Acme Corp"}'
  style="min-width: 320px; height: 700px;"
></div>
<script src="https://workspace.leenops.com/api/scheduling/embed.js" async></script>

Popup widget:

<script>
  Leenops.q.push(["initPopupWidget", {
    url: "https://workspace.leenops.com/book/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 (/poll/<token>, /one-off/<token>, /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

Leenops serves the embed and all its asset requests from a single origin: workspace.leenops.com. No additional Leenops application 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 Leenops entry, and your CSP connect-src only needs one origin. The first-party / third-party split: cookies set on workspace.leenops.com are first-party to Leenops, 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
@leenops/react-scheduling-embedReact component — <LeenopsEmbed url="..." prefill={...} />. Tree-shaken, no script tag injection.
@leenops/angular-scheduling-embedAngular module — <leenops-embed [url]="..." [prefill]="..."></leenops-embed>.
@leenops/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 Leenops 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("leenops: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
leenops:event_type_viewedThe widget finishes loading and the visitor sees the calendar
leenops:date_selectedThe visitor picks a date
leenops:event_scheduledA booking is confirmed — the most common one to wire
leenops: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 workspace.leenops.com, so cookies set there are first-party to Leenops 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.

Leenops 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 Leenops 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 Leenops 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