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
| Header | Description |
|---|---|
Content-Type | application/json; charset=utf-8 |
webhook-id | The 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-timestamp | When this attempt was sent, as a Unix timestamp in seconds, for example 1792488372. It changes on each retry. |
webhook-signature | The signature of this attempt, as 64 lowercase hexadecimal characters. See Check the signature. |
webhook-url-id | The 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:
| Field | Type | Description |
|---|---|---|
type | string | The enquiry type, for example care.create. Test events end in .test, for example care.create.test. See Enquiry Webhook Categories. |
timestamp | string | When the event was created, in ISO 8601 format with a time zone, for example 2026-10-20T09:26:10.344Z. |
data | object | The 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.
| Field | Type | Description |
|---|---|---|
id | integer | The 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.id | integer | The category ID. See Enquiry Webhook Categories. |
category.name | string | The category name, for example Care Enquiry. |
member.id | integer or string | The ID of the setting the enquiry was sent to. Store it as text, because large IDs can be sent as a string. |
member.name | string | The setting's name. |
member.postcode | string | The setting's postcode. |
group | object or null | The setting's group, with the same id, name and postcode fields as member. null if the setting is not in a group. |
emailto | string or null | The email address the enquiry was sent to. Always null for call.create. |
emailfrom | string or null | The enquirer's email address. |
entrydate | string | When 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. |
bodycontent | string | The 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. |
firstname | string or null | The enquirer's first name. |
surname | string or null | The enquirer's surname. |
address1, address2, address3 | string or null | The enquirer's address, if they gave one. Usually only collected for brochure requests. |
town | string or null | The enquirer's town. |
postcode | string or null | The enquirer's postcode. |
telephone | string or null | The enquirer's phone number. |
isAnonymised | string | Yes if the enquirer's personal details have been removed, otherwise No. |
categorySpecificData | object | Extra 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).
- Join the
webhook-idheader, thewebhook-timestampheader 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. - Calculate the HMAC-SHA256 of that text, using the secret key exactly as it is shown in the Portal.
- Write the result as lowercase hexadecimal and compare it with the
webhook-signatureheader, using a constant-time comparison. - Reject the request if they do not match. We also recommend rejecting requests whose
webhook-timestampis 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.