# Webhooks: send your bookings to Airtable

> Web version : https://www.calenoo.ch/en/help/webhooks
> Last updated : 2026-11-05
> [Français](https://www.calenoo.ch/fr/help/webhooks.md) · [Deutsch](https://www.calenoo.ch/de/help/webhooks.md) · [English](https://www.calenoo.ch/en/help/webhooks.md) · [Italiano](https://www.calenoo.ch/it/help/webhooks.md)

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.

## 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](https://www.calenoo.ch/en/app/account#webhooks), 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](https://www.calenoo.ch/en/guides/reservation-validation-groupes-webhooks).

## Step by step: Airtable

These steps follow Airtable's documentation of the “When webhook received” trigger ([source](https://support.airtable.com/docs/en/when-webhook-received-trigger), read on 2 November 2026). If Airtable's screen differs from what is described here, its help page prevails.

1. In your Airtable base, open “Automations” and create an automation.
2. Add a trigger and choose “When webhook received”. Airtable shows a unique web address: copy it.
3. In Calenoo, open [Settings › Webhooks](https://www.calenoo.ch/en/app/account#webhooks), paste the address into “Receiving address” and click “Add”.
4. Leave the “Sign deliveries” box unticked: Airtable cannot verify a signature.
5. Click “Send a test”. Calenoo sends a complete sample booking, with no empty field, and shows “Test received” when Airtable accepts it.
6. In Airtable, run the trigger's test (“Run test” button). Airtable uses the test delivery as the example for the next steps.
7. 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.id` as the identifier.
8. 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](https://support.airtable.com/docs/en/when-webhook-received-trigger) on 2 November 2026. These limits are Airtable's and may change.

- Only POST requests are accepted, with a `Content-Type` header set to `application/json` for 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.

```json
{
  "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.status` and `booking.previous_status`: the booking's status and its previous status (`null` if there is none).
- `booking.start` and `booking.end`: start and end in universal time (UTC). `booking.start_local` and `booking.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; `null` otherwise.
- `booking.location`, `booking.language` and `booking.meeting_url`: the page's location, the booking's language and the video call link (`null` if there is none). A pending request has no link yet: it arrives with `booking.accepted`.
- `booking.request_expires_at`: the deadline of a request awaiting approval (48 hours at most, never after the meeting starts), `null` otherwise. It stays filled in after the decision.
- `booking.seats`: the number of seats per slot on a group page, `null` for a one-to-one meeting.
- `booking.session_id`: the identifier of the session on a group page, the same for all its participants, `null` for 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`, `null` if there is none).
- `page`: the identifier, the short address (`slug`) and the title of the booking page.

### The headers

- `Content-Type: application/json`
- `User-Agent: Calenoo-Webhook/1`
- `Calenoo-Event`: the event, for example `booking.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.

```js
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-After` header (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](https://www.calenoo.ch/en/app/account#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](https://www.calenoo.ch/en/app/account#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](https://www.calenoo.ch/en/legal/privacy).

## 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](https://support.airtable.com/docs/en/when-webhook-received-trigger)).

### 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 `event` field (or the `Calenoo-Event` header) and keeps the ones it wants to handle.

### How do I avoid duplicates in my table?

Use `booking.id` as the key: a reschedule or a cancellation concerns the same booking, so it can update the same row. The `Calenoo-Delivery` header 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.
