How to embed a booking system into your web app

Stanislav TyshchenkoTutorial12 min readAug 11, 2026
Embedding a booking system into an existing web app

"Can we add booking to the site?" is one of those requests that sounds like a script tag and turns into a sprint. Sometimes it genuinely is a script tag. Sometimes the script tag is the thing that makes the project fail six weeks later, when the client asks why the calendar looks nothing like the rest of the site and why Safari users keep losing their basket.

There are four real ways to put booking inside an existing website or web app, and they're not points on a single scale — they trade different things away. This is a walk through all four with the code, plus the failure modes I've watched teams hit with each one. I build Opencals, a booking API and platform, so the API section uses it; the first three approaches are vendor-agnostic and the caveats apply whichever tool you're embedding.

Pick the embed approach by what has to match your brand and what has to survive a browser update. Linking out is the most reliable and the least yours. An iframe is fast but constrained by the vendor's CSP and by third-party cookie rules. A vendor widget looks native until you need it to behave differently. Building on a booking API costs the most upfront and is the only option that gives you full control of the UI and the data.

The four approaches at a glance

ApproachTime to shipDesign controlMain risk
Link out to a hosted booking pageAn hourNone beyond vendor themingCustomers leave your site mid-funnel
Iframe the hosted pageA dayLowVendor CSP, iframe height, third-party cookies
Vendor JavaScript widgetA day or twoMediumYou inherit the vendor's UX and bundle
Build on a booking API / SDKOne to three weeksTotalYou own the frontend forever

Two thirds of the projects I see should use approach 1 or 2 and don't, because "embedded" sounds more professional than "link". The other third try approach 2 for something that genuinely needed approach 4, and spend the difference in workarounds.

The vendor hosts the booking flow on their domain or a subdomain of yours. You link to it. That's the whole integration.

html
<a href="https://yourshop.opencals.com/book?service=beard-trim&staff=marek" class="btn btn-primary" > Book an appointment </a>

Most hosted booking pages accept query parameters to preselect a service, a staff member, a location, or a date. Use them. A link that lands on a generic service list converts noticeably worse than one that lands on the specific service the customer was just reading about — you already know what they clicked, so pass it along.

If the vendor supports a return URL, set it, so a completed booking sends the customer back to a page you control and you can fire your own conversion event there.

Pros

  • Nothing to maintain — the vendor ships fixes, you get them
  • No CSP, cookie, or iframe-height problems at all
  • Works identically on every browser and every screen size
  • Deep links let you preselect service, staff, and location

Cons

  • The customer leaves your domain in the middle of the funnel
  • Branding is limited to whatever theming the vendor exposes
  • Analytics needs cross-domain setup or you lose attribution
  • The booking page is rented — it isn't on your domain and doesn't leave with you

For a marketing site whose job is to get someone to book, this is very often the correct answer, and the "unprofessional" objection is mostly imaginary. Customers hand their card details to Stripe-hosted checkouts on a different domain every day.

Approach 2 — iframe the hosted page

An iframe keeps the customer on your page while the vendor renders the flow. It's a genuine middle ground, and it has three specific traps.

html
<div class="booking-frame"> <iframe src="https://yourshop.opencals.com/book" title="Book an appointment" loading="lazy" allow="payment" ></iframe> </div> <style> .booking-frame { position: relative; width: 100%; min-height: 720px; } .booking-frame iframe { position: absolute; inset: 0; width: 100%; height: 100%; border: 0; } </style>

Trap one: the vendor's CSP decides whether this works at all. Any booking page worth using sets a frame-ancestors directive so it can't be silently framed by a phishing site. If your domain isn't in that list, your iframe renders a blank box and a console error, and no amount of CSS fixes it. The Opencals storefront works this way — framing is allowlisted per domain rather than open by default, so you ask for your domain to be added before you build against it. Check this on day one with any vendor, not after the layout is done.

Trap two: height. A booking flow changes height as the customer moves from service list to calendar to checkout. A fixed-height iframe either clips the checkout or leaves 400px of dead space on the service list. The clean fix is postMessage — if the vendor emits resize events, listen for them:

javascript
window.addEventListener("message", (event) => { if (event.origin !== "https://yourshop.opencals.com") return; if (event.data?.type !== "resize") return; document.querySelector(".booking-frame").style.minHeight = `${event.data.height}px`; });

Always check event.origin. A message listener that trusts any sender is a real vulnerability, not a lint warning. If the vendor doesn't emit resize events, set a generous min-height and accept the whitespace — polling scrollHeight across origins is not possible, and the hacks that claim otherwise don't work anymore.

Trap three: third-party context. Inside an iframe, the vendor's cookies are third-party cookies. Safari's tracking prevention has blocked those for years, and Chrome's rules keep tightening. Vendors that depend on a session cookie to hold a basket across steps can drop bookings in the middle of checkout, and it reproduces only in the browsers you don't develop in. Test a full booking in Safari on an actual iPhone before you ship. Not the simulator, and not desktop Safari with default settings.

Test the iframe in Safari, on a phone, in a private window

Every iframe embed bug report I've seen started with "it works fine on my machine". Third-party storage restrictions, mobile keyboards resizing the viewport, and date pickers that expect the full screen all show up on a real phone and nowhere else. Ten minutes of testing here saves a support thread that runs for a month.

Approach 3 — a vendor JavaScript widget

A widget is a script the vendor gives you that mounts their UI into a container in your page. Calendly, Acuity and most appointment tools ship one; the typical shape is a container plus an init call.

html
<div id="booking"></div> <script src="https://vendor.example.com/widget.js" defer></script> <script> window.addEventListener("load", () => { VendorBooking.mount("#booking", { shop: "yourshop", service: "beard-trim", theme: "light", }); }); </script>

Under the hood, most widgets are still an iframe — they just manage its height and lifecycle for you, which removes trap two above but not traps one and three. Some render directly into your DOM, which is better for styling and worse for isolation: their CSS reset and yours will disagree about something eventually.

The real cost of a widget is that you inherit the vendor's product decisions. If their flow asks for a phone number and your client insists it's optional, you can't change it. If they load 180KB of JavaScript on a page that had a 40KB budget, that's now your Lighthouse score.

Worth being straight about this one: Opencals doesn't ship a universal drop-in script for arbitrary websites today. The widget exists inside the Shopify integration, where it installs as a theme block and inherits your Shopify theme. Everywhere else, the supported paths are the hosted storefront (approaches 1 and 2, with design customization for colours, fonts and logo) or the API. If a one-line script for any website is your hard requirement, a vendor that ships one is the better fit and I'd rather say so than sell you a workaround.

Approach 4 — build the flow on a booking API

The last option isn't embedding someone else's UI at all. You call a booking API for availability, cart, and payment, and you write the frontend yourself. Everything the customer sees is your code, on your domain, in your component library.

This is the headless approach, and it's the right call when the booking flow is part of the product rather than a page on a brochure site — when it needs your auth, your design system, or your own multi-step logic.

With Opencals, install the typed SDK and keep the key server-side:

bash
npm i @opencals/storefront-sdk
typescript
// lib/opencals.ts import { setupOpencals } from "@opencals/storefront-sdk"; export const opencals = setupOpencals({ apiKey: process.env.OPENCALS_API_KEY!, // sfk_live_... });

Then the flow is three calls: list what's bookable, ask for real availability, create the booking.

typescript
import { ProductService, AvailabilityService } from "@opencals/storefront-sdk"; import { opencals } from "@/lib/opencals"; // 1. What can be booked const { data: services } = await ProductService.listProducts({ opencals }); // 2. When is it actually free — computed from staff hours, // existing bookings, service duration and buffers const { data: slots } = await AvailabilityService.getAvailability({ opencals, productId: services[0].id, from: "2026-08-18", to: "2026-08-25", });

A storefront key can create real bookings and take payments, so it belongs in a Route Handler, a Server Action, or your own backend — never in a client bundle. Same rule as a Stripe secret key.

The availability call is the one that justifies the whole approach. Computing which slots are genuinely open means resolving staff schedules, service durations, buffers, existing bookings, group capacity and location hours together, and getting it race-safe so two customers can't hold the same slot. That's the part teams underestimate when they decide to build booking from scratch, and the part you're buying when you call an API instead.

For a complete walkthrough with the cart, checkout and confirmation screens wired up, see adding booking to a Next.js app with a REST API. The booking SDK reference covers the typed methods, and the booking API guide covers what to look for in any scheduling API, not just this one.

Pros

  • Complete control of the UI, the copy, and the number of steps
  • Booking lives on your domain, inside your auth and your analytics
  • One integration can serve web, mobile, and a Shopify store
  • No iframe, no third-party cookies, no CSP negotiation

Cons

  • One to three weeks of frontend work, not an afternoon
  • You own every bug in the booking UI from then on
  • Overkill for a brochure site whose only goal is a booked slot
  • Needs a developer available for changes the client used to self-serve

How to choose

1

Does the booking flow need your auth or your data?

If a logged-in customer should see their own history, pricing, or credits, an embedded vendor flow can't know who they are. Go to the API approach.

2

Does the design have to be exactly yours?

Not 'on-brand' — exactly yours, down to spacing and typography. If a themable vendor UI is acceptable, stop at the hosted page or a widget and save two weeks.

3

Who maintains it in a year?

If the client changes their own services and hours and has no developer, a hosted page they can configure themselves beats a custom frontend that needs a ticket for every change.

4

Check the vendor's frame-ancestors policy before designing

Framing an external booking page is only possible if the vendor allowlists your domain. Ask first — it decides whether approach 2 is on the table at all.

5

Test the whole flow on a real phone in Safari

Third-party storage, viewport resize on keyboard open, and native date pickers behave differently there. This is where embedded flows actually fail.

6

Decide where the conversion event fires

Whatever you choose, make sure a completed booking lands somewhere your analytics can see it, whether that's a return URL, a postMessage event, or your own confirmation route.

Where Opencals fits

Opencals is a booking and service-commerce platform with a public Storefront API, so it covers approaches 1, 2 and 4 directly, and approach 3 only inside Shopify.

If you want the fastest path: a hosted storefront on yourshop.opencals.com, styled with your colours, fonts and logo, linked from your site with deep links per service. If you want it framed inside your own page, that works once your domain is allowlisted. If you're an agency or a product team that needs the booking UI to be genuinely yours, the Storefront API and SDK give you the availability engine, cart, payments and post-booking management, and you build the rest — the MIT-licensed Next.js templates are a working starting point rather than a demo.

Pricing is $0.99 per completed booking or a custom monthly plan from $15, with no per-seat charge, which matters if you're an agency running this for several clients rather than one business.

Frequently Asked Questions

The short version

If the booking flow is a page on a marketing site, link to a hosted booking page and spend the saved time on the copy around it. If it has to stay visually inside your page and the vendor allowlists your domain, iframe it — and test it on a phone in Safari before you call it done. If booking is part of your product, build it on an API and accept that you now own the frontend.

The mistake worth avoiding is choosing approach 2 for a job that needed approach 4, then spending three weeks fighting postMessage and cookie policies to get 60% of what a proper API build would have given you in the same time. If you're not sure which side of that line you're on, get in touch — it's usually a ten-minute conversation.

Early Access — 3 Months Free

Ready to transform your service business?

Join 150+ businesses already using Opencals. Get 3 months completely free with all features unlocked.

No credit card required
Setup in 10 minutes
Cancel anytime