Developers

Integration Guide

Add bookings to your site in minutes. Choose iframe embed for zero-code, or the JavaScript SDK for full control.

What you need before starting

API keys for headless use

When calling the API directly (headless / custom UI), you need an API key with the right scopes.

  • Publishable key (zentier_publishable_…). safe for frontend code. Scoped to specific permissions like availability:read and bookings:write.
  • Server secret (zentier_server_secret_…). backend only. Has full access to all widget endpoints. Generate one in Settings → API keys by clicking Generate key and selecting Server secret. The key is shown once. copy it to your .env file immediately.

Never expose server secrets

Server secret keys bypass all scope checks. Use them only in server-side code, never in HTML, JavaScript bundles, or mobile apps.

Testing with Postman or cURL

The widget API is a standard REST API. You can test it with Postman, Insomnia, or cURL before writing any application code.

1

Get your keys

Go to Settings → API keys and copy your publishable key (or generate a server secret for full access). You also need your Tenant ID and a Resource ID from the dashboard.

2

Call availability

cURL
curl -sS "https://YOUR_APP_URL/api/widget/availability?key=YOUR_PUBLISHABLE_KEY&resource_id=YOUR_RESOURCE_ID&start_date=2026-06-01&end_date=2026-06-30"
Postman
GET https://YOUR_APP_URL/api/widget/availability?resource_id=YOUR_RESOURCE_ID&start_date=2026-06-01&end_date=2026-06-30

Headers:
  x-publishable-key: YOUR_PUBLISHABLE_KEY
3

Create a booking

cURL (free booking)
curl -sS -X POST "https://YOUR_APP_URL/api/widget/booking" \
  -H "Content-Type: application/json" \
  -H "x-publishable-key: YOUR_PUBLISHABLE_KEY" \
  -d '{
    "resource_id": "YOUR_RESOURCE_ID",
    "slot_id": "SLOT_UUID",
    "customer_name": "Jane Doe",
    "customer_email": "jane@example.com",
    "amount_cents": 0,
    "currency": "SEK"
  }'
Postman
POST https://YOUR_APP_URL/api/widget/booking

Headers:
  Content-Type: application/json
  x-publishable-key: YOUR_PUBLISHABLE_KEY

Body (raw JSON):
  {
    "resource_id": "YOUR_RESOURCE_ID",
    "slot_id": "SLOT_UUID",
    "customer_name": "Jane Doe",
    "customer_email": "jane@example.com",
    "amount_cents": 0,
    "currency": "SEK"
  }

Response codes

  • 200 OK. Success
  • 401 Unauthorized. Missing or invalid API key
  • 403 Forbidden. API key missing required scope
  • 409 Conflict. Slot already taken or insufficient capacity
  • 429 Too Many Requests. Rate limited

Option 1. Iframe Embed (Easiest)

Drop an iframe into any HTML page. No JavaScript required. The booking UI loads inside the iframe and handles availability, selection, and checkout automatically.

1

Copy the iframe code

Replace the placeholders with your actual values:

iframe embed
<iframe
  src="https://YOUR_APP_URL/embed/YOUR_TENANT_ID?key=YOUR_PUBLISHABLE_KEY&widget=YOUR_WIDGET_CONFIG_ID"
  width="100%"
  height="720"
  style="border:0;border-radius:12px;box-shadow:0 1px 3px rgba(0,0,0,0.1)"
  title="Bookings"
  allow="payment"
></iframe>
2

Add allowed origins (required for any external domain)

For security, every domain that loads the widget must be pre-registered. Go to Settings → Widget embed apps and add the full origin (scheme + host), e.g. https://www.example.com. The same allowlist is used for two things:

  • CORS. so the browser can call /api/widget/* from that site.
  • CSP frame-ancestors. so the browser allows the /embed/* page to be loaded inside an iframe from that site.

If a customer reports "frame-ancestors" ... blocked in DevTools, they need to add their domain here. Self-hosted installs can also set WIDGET_ALLOWED_ORIGINS as a platform-wide override.

3

Customize the look

Go to Dashboard → Widgets to set colors, button text, and layout. Changes apply instantly. no code redeploy needed.

Height tip

Use height="720" or more for the full booking flow. You can also use min-height in CSS and let the iframe scroll internally.

Option 2. JavaScript SDK (More Control)

Load /sdk/booking-widget.js with a script tag. The SDK can open a modal popup or render inline inside any element on your page.

1

Add the script tag

Script tag (modal mode. default)
<script
  src="https://YOUR_APP_URL/sdk/booking-widget.js"
  data-tenant="YOUR_TENANT_ID"
  data-key="YOUR_PUBLISHABLE_KEY"
  data-widget-config="YOUR_WIDGET_CONFIG_ID"
  data-api-url="https://YOUR_APP_URL/api"
  data-embed-url="https://YOUR_APP_URL"
  data-button-text="Book Now"
></script>
2

Choose a mode

The SDK supports two modes:

  • Modal (default). renders a button that opens a booking popup.
  • Inline. embeds the widget directly into a page element.
Inline mode
<div id="booking-widget"></div>

<script
  src="https://YOUR_APP_URL/sdk/booking-widget.js"
  data-tenant="YOUR_TENANT_ID"
  data-key="YOUR_PUBLISHABLE_KEY"
  data-widget-config="YOUR_WIDGET_CONFIG_ID"
  data-api-url="https://YOUR_APP_URL/api"
  data-embed-url="https://YOUR_APP_URL"
  data-mode="inline"
  data-target="booking-widget"
></script>
3

Listen for events (optional)

React to bookings, errors, and modal closes from your own code:

JavaScript
// Open the modal programmatically
window.BookingWidget.open();

// Listen for a successful booking
window.BookingWidget.on('bookingSuccess', (booking) => {
  console.log('Booking confirmed!', booking);
  // Send to analytics, show a thank-you message, etc.
});

// Listen for errors
window.BookingWidget.on('bookingError', (error) => {
  console.error('Booking failed:', error);
});

// Clean up when needed
// window.BookingWidget.destroy();

Option 3. Headless / Custom UI

Build your own UI and call the widget API directly. You have full control over every pixel.

Fetch availability
const params = new URLSearchParams({
  key: 'YOUR_PUBLISHABLE_KEY',
  resource_id: 'YOUR_RESOURCE_ID',
  start_date: '2026-06-01',
  end_date: '2026-06-30',
})

const res = await fetch(
  `https://YOUR_APP_URL/api/widget/availability?${params}`
)
const { slots } = await res.json()
Create a booking
const res = await fetch('https://YOUR_APP_URL/api/widget/booking', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'X-Publishable-Key': 'YOUR_PUBLISHABLE_KEY',
  },
  body: JSON.stringify({
    resource_id: 'YOUR_RESOURCE_ID',
    slot_id: 'SLOT_UUID',        // optional
    customer_name: 'Jane Doe',
    customer_email: 'jane@example.com',
    customer_phone: '+1234567890', // optional
    quantity: 1,
    amount_cents: 2500,
    currency: 'EUR',
  }),
})

const { booking, checkout_url } = await res.json()

if (checkout_url) {
  // Redirect to Mollie payment page
  window.location.href = checkout_url
}

Security note

Never put a zentier_server_secret_ key in frontend code. Server-only routes belong on your backend.

Troubleshooting

  • Widget config fails to load. Check DevTools → Network for widget/config. A 401 means the key is wrong; 403 means the widget is unpublished; CORS errors mean the origin is not in WIDGET_ALLOWED_ORIGINS.
  • Script not working on another domain. You must set both data-api-url and data-embed-url to your Zentier app origin.
  • Payments not redirecting. Ensure Mollie is connected under Dashboard → Payments.

Next steps