Webhook for Leads and Appointments: Send Requests to Zapier, Make, n8n or Your CRM

Webhook for leads and appointments

A webhook sends every new request to an address of your choice right away. Leads and appointments go straight into Zapier, Make, n8n or your own CRM, for example to open a ticket or email your sales team.

The webhook is available from the Standard plan, see the pricing page.

Set up

  1. Open Dashboard → your assistant → Functions.
  2. In the list, choose Pass on automatically (group Win customers). There you find the Webhook section.
  3. Enter the address, for example the "Catch Hook" URL from Zapier, and click Save.
  4. Copy the secret key. It is shown only this once. New key creates a new one if needed.
  5. Choose what to pass on: New contact requests, New appointments or both.
  6. Test sends a sample request to your address.

The address must start with https://, be publicly reachable and use port 443 or 8443. Addresses inside your own network (for example localhost or 192.168.…) are rejected.

Events

EventWhen
lead.createdA lead was saved, from the chat, a phone call or an Action Bar form.
booking.createdAn appointment was booked.

Example: new lead

{
  "id": "7a05fd6b-3b54-4235-b76e-7695c4ab13a3",
  "event": "lead.created",
  "createdAt": "2026-10-23T15:11:28.895Z",
  "data": {
    "leadId": "51f7f1e3-3e2f-471b-ade6-90959a5d3724",
    "source": "chat",
    "name": "Erika Example",
    "email": "erika@example.com",
    "phone": "+44 20 7946 0000",
    "interest": "Quote for a heat pump",
    "language": "en",
    "fields": { "Company": "Example Ltd" },
    "sessionId": "861f9c24-11ab-4352-b326-5670b0196fc0",
    "createdAt": "2026-10-23T15:11:28.742Z"
  }
}

source is chat, phone or form. Fields without a value are left out. fields holds your extra details keyed by their label. A callback request adds callback with day and time, a form adds formName.

Example: new appointment

{
  "id": "0c8f2d0e-6f1a-4a43-9a0b-2f9a3c1d5e77",
  "event": "booking.created",
  "createdAt": "2026-10-23T15:20:04.112Z",
  "data": {
    "bookingId": "e3f82cb1-4f6a-4d6e-9c55-0d1a2b3c4d5e",
    "source": "card",
    "appointmentType": { "id": "consultation", "title": "Free consultation", "durationMinutes": 30 },
    "start": "2026-10-28T14:00",
    "end": "2026-10-28T14:30",
    "startUtc": "2026-10-28T14:00:00.000Z",
    "endUtc": "2026-10-28T14:30:00.000Z",
    "timezone": "Europe/London",
    "name": "Max Example",
    "email": "max@example.com",
    "fields": { "Company": "Example Ltd" },
    "createdAt": "2026-10-23T15:20:04.050Z"
  }
}

start and end are local time in timezone, startUtc and endUtc the same instants in UTC. source is card (booked through the appointment card in the chat), chat or phone. Fields without a value are left out: name (if the name was not asked or not given), email, phone, note (what it is about), meetingLink (video call link, Google Calendar only) and sessionId. fields holds the answers to the extra details from Book appointments, keyed by their label.

Headers

HeaderContent
X-Webhook-IdUnique ID of the delivery, same as the id field. Use it to spot duplicates.
X-Webhook-Eventlead.created or booking.created
X-Webhook-TimestampTime of delivery in seconds since 1970
X-Webhook-Signaturesha256= plus HMAC-SHA256 over timestamp.rawBody with your secret key

Verify the signature

Check the signature before you use the data. Use the unchanged raw request body, not re-serialized JSON.

Node.js:

import { createHmac, timingSafeEqual } from 'node:crypto'

export function isValidWebhook(rawBody, headers, secret) {
  const timestamp = headers['x-webhook-timestamp']
  const expected = 'sha256=' + createHmac('sha256', secret).update(`${timestamp}.${rawBody}`).digest('hex')
  const received = String(headers['x-webhook-signature'] || '')
  const fresh = Math.abs(Date.now() / 1000 - Number(timestamp)) < 300
  return fresh && received.length === expected.length && timingSafeEqual(Buffer.from(received), Buffer.from(expected))
}

PHP:

$raw = file_get_contents('php://input');
$timestamp = $_SERVER['HTTP_X_WEBHOOK_TIMESTAMP'] ?? '';
$received = $_SERVER['HTTP_X_WEBHOOK_SIGNATURE'] ?? '';
$expected = 'sha256=' . hash_hmac('sha256', $timestamp . '.' . $raw, $secret);

if (!hash_equals($expected, $received) || abs(time() - (int) $timestamp) > 300) {
    http_response_code(401);
    exit;
}
$event = json_decode($raw, true);

In Zapier or Make you can skip the signature check as long as the address stays secret.

Response and retries

  • Reply with a 2xx status once you have accepted the data. We don't read the body of your reply.
  • We wait at most 5 seconds.
  • On timeouts, network errors, status 429 or 5xx we try again after 1 and after 5 seconds, so at most three times.
  • Other errors (for example 404) are not retried. Redirects are not followed.
  • The dashboard shows the outcome of the last delivery under Last delivery.

No lead is lost if your system doesn't answer: it is always in the Leads Dashboard first.