Iframe embed

Every booking page has an embeddable version at:

https://www.clndr.pro/embed/{username}/{slug}

It's the same booking flow as the hosted page, without clndr.pro's header and footer, and it allows framing from any site. This is what the embed script creates for you. Use the iframe directly when you can't run scripts, or when you want full control of the frame.

index.html
<iframe
  src="https://www.clndr.pro/embed/ada/intro-call?theme=auto"
  title="Book a call with Ada"
  style="width: 100%; min-height: 640px; border: 0"
></iframe>

Query parameters

theme'light' | 'dark' | 'auto'query

auto follows the guest's system setting.

hideEventTypeDetails'1'query

Hides the host's name, the page title, the duration and the description.

primaryColorstringquery

Any CSS colour for buttons and highlights. URL-encode it: #9A5A16 becomes %239A5A16.

namestringquery

Prefills the guest's name.

emailstringquery

Prefills the guest's email.

cstringquery

An access code, for private pages. Without it a private page shows "Booking page not found".

Build the URL with URLSearchParams so names and colours are encoded properly:

const src = new URL('https://www.clndr.pro/embed/ada/intro-call');
src.search = new URLSearchParams({ theme: 'dark', primaryColor: '#E0913D', email: 'grace@example.com' }).toString();

Sizing

The page's height changes as the guest moves from the date picker to the slots to the form. The frame can't know that by itself, so either give it a fixed height (640px fits the date and slot steps on most screens) or listen for the page's resize messages:

index.html
<iframe id="clndr" src="https://www.clndr.pro/embed/ada/intro-call" title="Book a call"
        style="width: 100%; min-height: 600px; border: 0" scrolling="no"></iframe>
 
<script>
  window.addEventListener('message', (event) => {
    if (event.origin !== 'https://www.clndr.pro') return;
    const frame = document.getElementById('clndr');
    if (event.source !== frame.contentWindow) return;
    if (event.data?.type === '__clndr:resize') {
      frame.style.height = event.data.payload.height + 'px';
    }
  });
</script>

Check event.origin on every message. The embed posts to *, so any page that frames it receives the messages, and you should only trust the ones that come from clndr.pro.

Events

The embed posts { type, payload } messages to its parent window:

typepayloadSent when
__clndr:resize{ height: number }On load, and whenever the content's height changes
__clndr:route_changed{ path: string }When the booking page loads, for example /embed/ada/intro-call
__clndr:booking_successful{ meetingId?: string, startTime: string, endTime: string }After the guest books; times are UTC ISO strings
window.addEventListener('message', (event) => {
  if (event.origin !== 'https://www.clndr.pro') return;
  if (event.data?.type === '__clndr:booking_successful') {
    const { meetingId, startTime } = event.data.payload;
    console.log('Booked', meetingId, 'for', new Date(startTime).toLocaleString());
  }
});

On approval pages booking_successful means the request was sent; the booking is pending until the host confirms it.

Private and team pages

A private page needs its access code in c. Anyone who can read your page's HTML can read that code too, so treat an embedded access code as shared with your site's visitors.

team pages don't work inside an iframe. They need the guest to be signed in to clndr.pro, and browsers don't send clndr.pro's cookies to a frame on another site. Link to the hosted page instead.

Quirks worth knowing

  • A select question shows as a plain text input in the embed, a phone question as a text input, and a checkbox question as a checkbox whose answer is always saved empty. If a page uses select or checkbox questions, render the form yourself with @clndr-pro/react.
  • The embed asks for slots by the guest's local midnight, and clndr.pro answers with the host's day that contains that moment. When the guest and the host are many hours apart, the times under a date can fall on the day before or after it in the guest's time. The API takes a host-timezone date and leaves out past slots, so custom integrations don't have this problem.