# Webhooks

Send every new enquiry and show-round booking from Toddly to Zapier, Make or your own system as a signed POST. Part of Pro.

Updated 14 November 2026. Page: https://toddly.co.uk/for-nurseries/webhooks

A webhook sends each event to an address you choose, the moment it happens, so a new enquiry can land in your spreadsheet, CRM or nursery management system without anyone copying it across.

## Events

| Type | When |
|---|---|
| `enquiry.created` | A parent sends an enquiry, visit request or callback request |
| `visit.booked` | A show-round is booked: by the parent into one of your times, or by you from the enquiry page |
| `visit.moved` | The parent moves their show-round to another time |
| `visit.cancelled` | The show-round is cancelled, by the parent or by you |
| `test` | You press "Send test event" |

## Set it up

1. In your dashboard open **Settings**, then **Webhook**.
2. Paste the https address your tool gives you and press **Save**.
3. Copy the **signing secret** we show you. It is shown once only; you can make a new one at any time.
4. Press **Send test event**. The last delivery and its result show under the address.

Only public https addresses are accepted: no IP addresses, no ports other than 443, nothing on a private network. Redirects are not followed.

### Zapier

1. Create a Zap and choose **Webhooks by Zapier** as the trigger, with the event **Catch Hook**.
2. Copy the webhook URL Zapier shows and paste it into Toddly under Settings, Webhook.
3. Press **Send test event** in Toddly, then **Test trigger** in Zapier. Zapier now knows the fields: map `data.enquiry.parent_name`, `data.enquiry.parent_email` and the rest into your next step.

### Make

1. Create a scenario and add the **Webhooks** module **Custom webhook**. Press **Add** and copy the address Make creates.
2. Paste it into Toddly under Settings, Webhook.
3. Press **Send test event** in Toddly while Make is listening, so it learns the data structure.

## What we send

Each event is a `POST` with a JSON body:

```json
{
  "id": "evt_6f1c9a0d2b7e4c3a8f5e1d20",
  "type": "enquiry.created",
  "created": 1789466400,
  "nursery_id": "eng-ey490982",
  "data": {
    "enquiry": {
      "id": 1234,
      "type": "visit",
      "child_age_months": 20,
      "start": "2027-01",
      "days": ["mon", "wed"],
      "message": "Could we look round on a Tuesday morning?",
      "parent_name": "Sam Jones",
      "parent_email": "sam@example.com",
      "parent_phone": "+447700900123",
      "visit_at": "2026-11-17T10:00:00Z",
      "link": "https://toddly.co.uk/dash/eng-ey490982/enquiries/1234"
    },
    "visit": {
      "id": 56,
      "starts_at": "2026-11-17T10:00:00Z",
      "status": "booked"
    }
  }
}
```

- `visit` is there on `visit.*` events; `visit.moved` adds `previous_starts_at`.
- `start` is a month (`2027-01`), `asap` or `not_sure`. Times carry their UK offset. A field the parent left out is left out or empty, never a guess.
- `id` is the same on every retry of one event: use it to ignore a repeat.

The body holds the same details as your enquiry email. Your nursery is the controller of them once they reach you, and the tools you send them to work for you.

## Check the signature

Every request has a header:

```text
X-Webhook-Signature: t=1789466400,v1=5f2b…
```

`v1` is the HMAC-SHA256 of the text `t`, a full stop and the raw request body, keyed with your signing secret, in hex. Recompute it, compare in constant time, and refuse a `t` more than five minutes old.

```js
const crypto = require("crypto");

function verified(header, rawBody, secret) {
  const parts = Object.fromEntries(header.split(",").map((p) => p.split("=")));
  const want = crypto.createHmac("sha256", secret).update(parts.t + "." + rawBody).digest("hex");
  const fresh = Math.abs(Date.now() / 1000 - Number(parts.t)) < 300;
  return fresh && want.length === parts.v1.length &&
    crypto.timingSafeEqual(Buffer.from(want), Buffer.from(parts.v1));
}
```

```python
import hashlib, hmac, time

def verified(header: str, raw_body: bytes, secret: str) -> bool:
    parts = dict(p.split("=", 1) for p in header.split(","))
    want = hmac.new(secret.encode(), parts["t"].encode() + b"." + raw_body, hashlib.sha256).hexdigest()
    fresh = abs(time.time() - int(parts["t"])) < 300
    return fresh and hmac.compare_digest(want, parts["v1"])
```

Zapier's and Make's catch hooks do not check signatures; the address they give you is long and unguessable, so keep it private.

## Delivery

- Answer with any `2xx` status within 5 seconds. We do not read your response body.
- Anything else, a redirect or no answer is a failure. We try again after 1, 4, 16, 64 and 256 minutes, six tries in all.
- After 20 failed deliveries in a row the webhook switches off and your team gets an email. Fix the address and send a test event to switch it back on.

## Frequently asked questions

### Which plan has webhooks?

Pro, and the 14-day Pro trial. On Free nothing is sent.

### Do I still get enquiry emails?

Yes. The webhook is an extra copy for your other tools. Enquiries still reach you by email and in your dashboard.

### What happens if my endpoint is down?

We try again over about six hours. After 20 failed deliveries in a row we switch the webhook off and email your team; a successful test event switches it back on.

