Webhooks: send your bookings to Airtable
A webhook automatically sends every booking, reschedule and cancellation on your Calenoo pages to another tool: an Airtable base, or any tool that can receive an HTTPS webhook (Zapier, Make, n8n…). This page explains how to set it up, what Calenoo sends and what happens when something goes down.
Last updated :
What webhooks are for
A webhook automatically sends the details of every booking, reschedule and cancellation to the web address of your choice, as soon as it happens.
The tool that receives them can file them in a table, notify someone or start other actions. Calenoo only does the sending: the rest is set up in that tool.
You set it up once, in your Settings, and it applies to all your booking pages. You can save up to 3 addresses.
All you need is a Calenoo account: the “Webhooks” section is in your Settings, like booking pages.
Webhooks combine with approving requests and group sessions, two other advanced features of booking pages. See the guide to the advanced features.
Step by step: Airtable
These steps follow Airtable's documentation of the “When webhook received” trigger (source, read on 2 November 2026). If Airtable's screen differs from what is described here, its help page prevails.
- In your Airtable base, open “Automations” and create an automation.
- Add a trigger and choose “When webhook received”. Airtable shows a unique web address: copy it.
- In Calenoo, open Settings › Webhooks, paste the address into “Receiving address” and click “Add”.
- Leave the “Sign deliveries” box unticked: Airtable cannot verify a signature.
- Click “Send a test”. Calenoo sends a complete sample booking, with no empty field, and shows “Test received” when Airtable accepts it.
- In Airtable, run the trigger's test (“Run test” button). Airtable uses the test delivery as the example for the next steps.
- Add the actions that will use this data and map the received fields to them. To find a rescheduled or cancelled booking again, use
booking.idas the identifier. - Test each step, then turn the automation on (from OFF to ON). From then on, every booking on your pages goes to Airtable.
What Airtable says about this trigger
Read on Airtable's help page on 2 November 2026. These limits are Airtable's and may change.
- Only POST requests are accepted, with a
Content-Typeheader set toapplication/jsonfor JSON. That is what Calenoo sends. - A request must not exceed 100 kb, and the trigger accepts at most five requests per second.
- Each delivery that starts an automation counts toward the automation run limits of your Airtable plan. The trigger is available on all plan types, with varying limits.
- Airtable does not support signature verification: keep Calenoo's signature turned off for Airtable.
- Anyone who knows the trigger's address can start your automation. Don't share it.
The events sent
Calenoo sends all the events in the table, with no way to select: your tool reads the event field and keeps the ones it needs.
| Event | When it is sent |
|---|---|
booking.created | A guest books on a page without approval. |
booking.requested | A guest sends a request on an “approval required” page (status is pending, request_expires_at gives the deadline). |
booking.accepted | You accept the request (status becomes confirmed, previous_status is pending; the video call link is added if there is one). |
booking.declined | You decline the request (status is declined). |
booking.expired | The request got no answer before the deadline (status is expired). |
booking.rescheduled | The guest moves their meeting or request (previous_start gives the old start; a request stays pending). |
booking.cancelled | The guest cancels their meeting or withdraws their request, or you cancel from Calenoo (previous_status gives the earlier status). |
Each delivery concerns a single booking. On a group page (several seats per slot), each participant gets a separate delivery, with their own booking.id and the booking.session_id of their session, which is the same for all its participants.
The data format
Each delivery is a POST request whose body is JSON (version 1). Here is an example. The “Send a test” button sends the same format, with test set to true and every field filled in.
{
"version": 1,
"event": "booking.created",
"event_id": "5d0c6b0e-6b0a-4a52-9d5c-2f1d3c9a7e11",
"occurred_at": "2026-11-02T18:00:00Z",
"test": false,
"booking": {
"id": "b3f1c2a4-8d7e-4c6b-9a10-5e2f4d8c1a77",
"status": "confirmed",
"previous_status": null,
"start": "2026-11-07T09:00:00Z",
"end": "2026-11-07T10:00:00Z",
"start_local": "2026-11-07T10:00:00+01:00",
"end_local": "2026-11-07T11:00:00+01:00",
"timezone": "Europe/Zurich",
"duration_minutes": 60,
"previous_start": null,
"location": "Bern",
"meeting_url": null,
"language": "de",
"request_expires_at": null,
"seats": null,
"session_id": null,
"invitee": {
"name": "Jeanne Fit",
"email": "jeanne@example.ch",
"note": "Looking forward to it"
}
},
"page": {
"id": "9a4e7d52-3c1b-4f08-8e66-0b7d2a5c9f33",
"slug": "personal-training",
"title": "Personal Training"
}
}The main fields
event: the event (see the table above).event_id: an identifier shared by all addresses that receive the same event.occurred_at: the time of the event, in universal time (UTC).booking.id: the booking's identifier. It is the key to use to find it again.booking.statusandbooking.previous_status: the booking's status and its previous status (nullif there is none).booking.startandbooking.end: start and end in universal time (UTC).booking.start_localandbooking.end_local: the same times, with the local time and offset of the page's time zone (booking.timezone).booking.duration_minutes: the duration in minutes.booking.previous_start: the old start, for a reschedule;nullotherwise.booking.location,booking.languageandbooking.meeting_url: the page's location, the booking's language and the video call link (nullif there is none). A pending request has no link yet: it arrives withbooking.accepted.booking.request_expires_at: the deadline of a request awaiting approval (48 hours at most, never after the meeting starts),nullotherwise. It stays filled in after the decision.booking.seats: the number of seats per slot on a group page,nullfor a one-to-one meeting.booking.session_id: the identifier of the session on a group page, the same for all its participants,nullfor a one-to-one meeting. It is kept after a cancellation; a reschedule gives the new session's identifier.booking.invitee: the guest's name, email address and message (note,nullif there is none).page: the identifier, the short address (slug) and the title of the booking page.
The headers
Content-Type: application/jsonUser-Agent: Calenoo-Webhook/1Calenoo-Event: the event, for examplebooking.created.Calenoo-Delivery: the delivery's identifier. It stays the same on every retry: use it to ignore a duplicate.Calenoo-Signature: present only if signing is turned on (see below).
The structure of version 1 does not change: any change would go through a version 2.
Verifying the signature
This step is optional. If you tick “Sign deliveries”, Calenoo adds a Calenoo-Signature header to every delivery: your tool can then check that the delivery really comes from Calenoo and hasn't been altered.
The header looks like t=<seconds>,v1=<signature>. t is the sending time in seconds (Unix time). v1 is an HMAC-SHA256 in hexadecimal, computed with your secret over the text t.body: t, a dot, then the request body exactly as received.
The secret starts with whsec_: use it in full, prefix included. Calenoo shows it only once, when it is created: copy it right away. If you lose it, create a new one with “New secret”.
Here is an example in JavaScript (Node.js). Always compute the signature over the raw text received, before any JSON.parse. Also reject a delivery whose t is too old: here, more than 5 minutes.
import crypto from "node:crypto";
function isValidSignature(rawBody, header, secret) {
const parts = Object.fromEntries(header.split(",").map((part) => part.split("=")));
const expected = crypto
.createHmac("sha256", secret)
.update(`${parts.t}.${rawBody}`)
.digest("hex");
const received = Buffer.from(parts.v1 ?? "", "hex");
const computed = Buffer.from(expected, "hex");
const fresh = Math.abs(Date.now() / 1000 - Number(parts.t)) < 300;
return fresh && received.length === computed.length && crypto.timingSafeEqual(received, computed);
}Node.js lowercases header names: the header is read with req.headers['calenoo-signature'].
When something goes down
A delivery succeeds when your tool answers with a 2xx code within 10 seconds. Calenoo never reads the content of the answer and follows no redirect: a 3xx answer counts as a failure.
- Answer 408, 425, 429 or 5xx, timeout or network drop: Calenoo tries again, 8 attempts at most (the first one included), after 1, 2, 5, 15 and 30 minutes, then 1 hour and 2 hours. If the answer contains a
Retry-Afterheader (in seconds) longer than that wait, Calenoo waits for it, up to 6 hours. - Any other 4xx answer (404 or 401, for example): final failure, no new attempt.
- Deliveries to one address go out in order: a delivery waiting for a retry holds back the following ones.
- After 5 final failures in a row, Calenoo disables the address and emails you. A successful delivery resets the counter to zero.
- Missed deliveries are not sent again when you reactivate the address. Once the problem is fixed, click “Reactivate” in Settings › Webhooks.
The same delivery can arrive twice, for example if your tool received it but its answer was lost. Since Calenoo-Delivery is the same on every attempt, your tool can ignore a duplicate.
The “Latest deliveries” log in Settings › Webhooks shows each delivery's event, response code and status. It is kept for 30 days, without the content of the deliveries.
Security and personal data
- Calenoo sends only over
https, on port 443. An address with a username or password, or on another port, is refused. - Private or local addresses (internal network,
localhost…) are refused when saved, then checked again before every delivery. - Never in the deliveries: the links and tokens used to manage the booking, the IP fingerprint, your email address and the references of your calendar.
- The content of each delivery is erased as soon as the delivery is finished.
- You stay in control: you can pause an address, sign it, change its secret or delete it at any time.
Each delivery contains personal data: the guest's name, email address and message, with the time of the booking. It goes to the tool you chose, which handles it under its own responsibility and its own terms. See the privacy policy.
Frequently asked questions
- Do I need a paid Airtable plan?
- Airtable states that the “When webhook received” trigger is available on all plan types, with varying limits. Each delivery that starts an automation counts toward your plan's automation run limits: check them in your Airtable account (source).
- My tool is down: do I lose bookings?
- No, bookings stay in Calenoo. Only the delivery to your tool is affected: Calenoo tries up to 8 times, waiting from 1 minute to 2 hours. After 5 final failures in a row, the address is disabled and missed deliveries are not sent again.
- Can I receive only some of the events?
- No: Calenoo sends all events. Your tool reads the
eventfield (or theCalenoo-Eventheader) and keeps the ones it wants to handle. - How do I avoid duplicates in my table?
- Use
booking.idas the key: a reschedule or a cancellation concerns the same booking, so it can update the same row. TheCalenoo-Deliveryheader identifies each delivery and stays the same on every retry. - Can I send to a server on my internal network or on another port?
- No. Calenoo only sends over HTTPS, on port 443, to a public address.