Enquiry Webhook Request

Prev Next

Each enquiry webhook is an HTTP POST request to your URL with signed headers and a JSON body. This page describes the request, how to check its signature and how your system should reply.

Who can use it: enquiry webhooks are available to Enhanced and Platinum subscribers. See Enquiry Webhooks Overview and Enquiry Webhook Setup.

Headers

HeaderDescription
Content-Typeapplication/json; charset=utf-8
webhook-idThe message ID, 32 letters and numbers. It stays the same if we retry the same message, so you can use it to process each message only once.
webhook-timestampWhen this attempt was sent, as a Unix timestamp in seconds, for example 1792488372. It changes on each retry.
webhook-signatureThe signature of this attempt, as 64 lowercase hexadecimal characters. See Check the signature.
webhook-url-idThe ID of the webhook this request was sent for. It matches the Webhook URL ID in the Portal.

Body

The body is a JSON object with three properties:

FieldTypeDescription
typestringThe enquiry type, for example care.create. Test events end in .test, for example care.create.test. See Enquiry Webhook Categories.
timestampstringWhen the event was created, in ISO 8601 format with a time zone, for example 2026-10-20T09:26:10.344Z.
dataobjectThe enquiry. See Enquiry data.
{
  "type": "care.create",
  "timestamp": "2026-10-20T09:26:10.344Z",
  "data": {
    "id": 12345678,
    "category": { "id": 4, "name": "Care Enquiry" },
    ...
  }
}

Enquiry data

Every enquiry type has the fields below. Fields that were not collected for an enquiry are sent as null.

FieldTypeDescription
idintegerThe enquiry ID. For call.create this is the ID of the call record, which is a separate series of numbers, so use type and id together to identify an enquiry.
category.idintegerThe category ID. See Enquiry Webhook Categories.
category.namestringThe category name, for example Care Enquiry.
member.idinteger or stringThe ID of the setting the enquiry was sent to. Store it as text, because large IDs can be sent as a string.
member.namestringThe setting's name.
member.postcodestringThe setting's postcode.
groupobject or nullThe setting's group, with the same id, name and postcode fields as member. null if the setting is not in a group.
emailtostring or nullThe email address the enquiry was sent to. Always null for call.create.
emailfromstring or nullThe enquirer's email address.
entrydatestringWhen we received the enquiry, or when the call started for call.create, in ISO 8601 format in UTC, for example 2026-10-20T09:26:07.000Z. For the time the event was created, use timestamp.
bodycontentstringThe enquiry as it appears in the enquiry email. It can be plain text or HTML. For tour.create and call.create it is an HTML summary of the request or call.
firstnamestring or nullThe enquirer's first name.
surnamestring or nullThe enquirer's surname.
address1, address2, address3string or nullThe enquirer's address, if they gave one. Usually only collected for brochure requests.
townstring or nullThe enquirer's town.
postcodestring or nullThe enquirer's postcode.
telephonestring or nullThe enquirer's phone number.
isAnonymisedstringYes if the enquirer's personal details have been removed, otherwise No.
categorySpecificDataobjectExtra fields that depend on the enquiry type. See the example page for each type, listed in Enquiry Webhook Categories.

Check the signature

Check the signature of every request before you use the data. You need the webhook's secret key from the Portal (APIs & Integrations → Webhooks, Secret column).

  1. Join the webhook-id header, the webhook-timestamp header and the raw request body with full stops: {webhook-id}.{webhook-timestamp}.{body}. Use the body exactly as you received it, before parsing it as JSON.
  2. Calculate the HMAC-SHA256 of that text, using the secret key exactly as it is shown in the Portal.
  3. Write the result as lowercase hexadecimal and compare it with the webhook-signature header, using a constant-time comparison.
  4. Reject the request if they do not match. We also recommend rejecting requests whose webhook-timestamp is more than 5 minutes from your current time.

Our header names follow the Standard Webhooks naming.

For example, with the webhook ID 3f2c9a7b1e4d4f0a8b6c5d2e1f0a9b8c, the timestamp 1792488372 and the body {"type":"care.create","timestamp":"2026-10-20T09:26:10.344Z","data":{"id":12345678}}, the text to sign is:

3f2c9a7b1e4d4f0a8b6c5d2e1f0a9b8c.1792488372.{"type":"care.create","timestamp":"2026-10-20T09:26:10.344Z","data":{"id":12345678}}

Node.js

const crypto = require('crypto');

function isValidWebhook(headers, rawBody, secret) {
  const signedText = `${headers['webhook-id']}.${headers['webhook-timestamp']}.${rawBody}`;
  const expected = crypto.createHmac('sha256', secret).update(signedText, 'utf8').digest('hex');
  const received = String(headers['webhook-signature'] || '');

  return received.length === expected.length
    && crypto.timingSafeEqual(Buffer.from(received), Buffer.from(expected));
}

PHP

$rawBody = file_get_contents('php://input');
$signedText = $_SERVER['HTTP_WEBHOOK_ID'] . '.' . $_SERVER['HTTP_WEBHOOK_TIMESTAMP'] . '.' . $rawBody;
$expected = hash_hmac('sha256', $signedText, $secret);

if (!hash_equals($expected, $_SERVER['HTTP_WEBHOOK_SIGNATURE'] ?? '')) {
    http_response_code(401);
    exit;
}

Reply to the request

Reply with any 2xx status code, such as 200 or 204, within 15 seconds. We do not read the response body. If you need to do slow work with the enquiry, save it first, reply, then do the work.

Use your final URL in the Portal. Do not redirect it to another address.

Retries and duplicates

If your system replies with any other status code, does not reply within 15 seconds or cannot be reached, we try again a limited number of times, then stop. Each attempt is shown in Webhook Logs in the Portal.

A retry has the same webhook-id and body as the first attempt, but a new webhook-timestamp and webhook-signature.

Occasionally you may receive the same enquiry more than once, for example if your system saved it but replied too late. Use type and data.id together to recognise an enquiry you already have. Enquiries can also arrive in a different order from the one they were sent in.