NotaryCentral supports a fully embedded booking experience, allowing you to host the client appointment scheduler and payment checkout on your own domain while secure scheduling operations run inside an iframe.
Why embed the booking portal?
Embedding the booking portal on your website allows you to provide a unified, premium scheduling experience for your clients. Instead of redirecting them away to a generic booking link, they remain on your trusted domain throughout:
- Selecting notary services or packages.
- Checking real-time date and time availability.
- Providing participant details and uploading documents.
- Completing secure Stripe card payments if required.
- Viewing booking confirmations and receipt summaries.
Step 1: Configure Settings
Before embedding the booking portal on your website, you must approve your parent website's domain in your NotaryCentral settings. This is a critical security step that configures the Content Security Policy (CSP) and allows your site to frame the calendar without browser blocks.
Getting started in the app
In app.notarycentral.org:
- Open the NotaryCentral app on your desktop or mobile device.
- Tap or click on the Menu.
- Search for Embedded customer experience settings.
- If you are navigating directly, the page path is /search/settings/embed-customer-experience.
- Scroll down to the Booking/calendar experience section.
What each field means
Enable embed: Toggle this switch to ON to allow NotaryCentral calendar flows to be loaded inside an iframe.
Booking embedder page URL: Enter the exact page URL where your embedded calendar iframe will live. Use the full HTTPS page URL in production, for example:
https://yourdomain.com/book
For local development, this may be:
http://localhost:5175/
Do not add query strings or trailing fragments in this settings field.
Booking allowed origin: Enter only the origin of your parent page if you want to explicitly allow a parent website origin. The origin is the scheme, host, and optional port, with no path, query string, or fragment. For example:
https://yourdomain.com
For local development, this may be:
http://localhost:5175
This should match the origin of the page that hosts the iframe.
Important: All URLs must include the http:// or https:// protocol. Do not add credentials or trailing fragments in these settings fields.
Step 2: Set Up the Developer Sample Code
To make the integration as seamless as possible, we provide a complete reference implementation on GitHub: NotaryCentral Embedded Booking React Sample at https://github.com/NotaryCentral/embedded-booking-react-sample.
This sample demonstrates two golden principles of premium embedding:
- Dumb Parent Parameter Forwarding: The parent container is completely dumb. It simply takes all query parameters from its own URL (including subdomain overrides and Stripe's post-payment redirect nc_path) and forwards them straight to the iframe. The iframe handles its own internal sub-routing.
- Dynamic Height Telemetry: The parent listens to notarycentral.calendar.resize events broadcasted from the iframe to dynamically resize its height, completely eliminating nested scrollbars and providing a native-app feel.
Reference React Implementation (App.tsx):
import { useState, useEffect } from 'react'
import './App.css'
function App() {
const [loaded, setLoaded] = useState(false)
const [iframeUrl, setIframeUrl] = useState('')
const [iframeHeight, setIframeHeight] = useState('800px')
useEffect(() => {
if (typeof window !== 'undefined') {
const parentOrigin = window.location.origin
const baseUrl = 'http://localhost:3001' // Your local or production calendar URL
// 1. DUMB PARENT FORWARDING: Grab all query parameters from parent URL
const searchParams = new URLSearchParams(window.location.search)
// Provide default fallbacks for local testing if parameters aren't supplied
if (!searchParams.has('subdomain') && !searchParams.has('user')) {
searchParams.set('subdomain', 'user56331')
}
searchParams.set('embed', 'true')
searchParams.set('parentOrigin', parentOrigin)
// Set the final iframe source URL with all search parameters preserved
setIframeUrl(`${baseUrl}/?${searchParams.toString()}`)
}
// 2. DYNAMIC RESIZE TELEMETRY: Listen for height changes from the iframe
const handleMessage = (event: MessageEvent) => {
const data = event.data
if (data && data.source === 'notarycentral') {
if (data.event === 'notarycentral.calendar.resize' && data.payload?.height) {
setIframeHeight(`${data.payload.height}px`)
}
}
}
window.addEventListener('message', handleMessage)
return () => window.removeEventListener('message', handleMessage)
}, [])
if (!iframeUrl) {
return null
}
return (
<div className="app-container">
{/* Optional: Premium Animated Loader Screen */}
<div className={`loader-overlay ${loaded ? 'fade-out' : ''}`}>
<div className="spinner" />
<p>Connecting to Booking Portal...</p>
</div>
{/* Embedded Booking Iframe */}
<iframe
key={iframeUrl}
src={iframeUrl}
style={{ height: iframeHeight }}
className={`booking-iframe ${loaded ? 'loaded' : ''}`}
onLoad={() => setLoaded(true)}
allow="payment; camera; microphone; display-capture; geolocation; clipboard-read; clipboard-write; fullscreen"
title="NotaryCentral Embedded Booking"
/>
</div>
)
}
export default App
Reference CSS Layout (App.css):
#root {
width: 100% !important;
max-width: 100% !important;
margin: 0 !important;
border-inline: none !important;
}
body, html {
margin: 0;
padding: 0;
width: 100%;
min-height: 100%;
background-color: transparent;
}
.app-container {
position: relative;
width: 100%;
min-height: 100vh;
background: transparent;
}
.booking-iframe {
width: 100%;
border: none;
display: block;
opacity: 0;
transition: opacity 0.8s ease-in-out, height 0.2s ease-out;
}
.booking-iframe.loaded {
opacity: 1;
}
Required Iframe Permissions
Because the booking calendar can host interactive features, you must declare appropriate permissions in the allow attribute of your iframe:
allow="payment; camera; microphone; display-capture; geolocation; clipboard-read; clipboard-write; fullscreen"
payment: Required to safely authorize Stripe Checkout sessions inside the framed environment.
camera and microphone: Highly recommended so your clients can test their setup or complete system prerequisites prior to their actual appointments.
Best Practices & Troubleshooting
Why is my calendar showing a Booking Calendar Unavailable error?
Verify that you have provided the correct subdomain query parameter. If you are developing locally, your parent page must forward the subdomain (or user) parameter down to the iframe. Our sample automatically falls back to user56331 for local testing.
How do I prevent nested scrollbars?
Never set a hard height on your iframe or set overflow: hidden on your parent containers. By listening to the notarycentral.calendar.resize postMessage events, your parent page will dynamically scale the iframe's height to match its exact content, allowing your users to scroll naturally using only the parent window's scrollbar.
What happens after payment completion?
Upon completing a Stripe checkout, Stripe redirects the client back to your parent website's URL. The redirect URL will contain an nc_path query parameter containing the path to the booking confirmation screen. Because of our dumb parent forwarding design, the parent app will automatically load that new path directly inside the iframe, loading the confirmation screen flawlessly!