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
- Your Tenant ID and Publishable Key from Settings → API Keys
- Your Widget Config ID from Dashboard → Widgets (optional, for theming)
- Your app URL (e.g.
https://your-app.vercel.app)
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 likeavailability:readandbookings: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.envfile immediately.
Never expose server secrets
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.
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.
Call availability
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"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_KEYCreate a 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"
}'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.
Copy the iframe code
Replace the placeholders with your actual values:
<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>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.
Customize the look
Go to Dashboard → Widgets to set colors, button text, and layout. Changes apply instantly. no code redeploy needed.
Height tip
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.
Add the script tag
<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>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.
<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>Listen for events (optional)
React to bookings, errors, and modal closes from your own code:
// 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.
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()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
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 inWIDGET_ALLOWED_ORIGINS. - Script not working on another domain. You must set both
data-api-urlanddata-embed-urlto your Zentier app origin. - Payments not redirecting. Ensure Mollie is connected under Dashboard → Payments.
Next steps
- API Reference. full endpoint list with request/response examples.
- Widget Styling. customize colors, fonts, and layout.
- Webhooks. get notified when bookings are created or paid.