Webhooks for New Leads
A webhook sends each new lead from your chat channels, such as website chat, Messenger and Instagram, to another app as soon as it is captured: your CRM, Zapier, Make, n8n or your own code. KnowStack makes a signed HTTPS POST with the lead's contact details and the conversation up to that moment.
Lead webhooks are included on Business, Enterprise and the free trial. You set them up if your role can manage users, such as Company Admin.
Add a webhook
- Copy the webhook address from the app that should receive the leads. It must start with https://.
- In KnowStack, open Settings, select 'Integrations' and go to 'Send leads to your CRM'.
- Under 'Add a webhook', enter a 'Name' you will recognise, paste the 'Webhook address' and click 'Add webhook'.
- Copy the signing secret that appears. It starts with whsec_ and is shown only once. Then click 'I've saved it'.
- Open the webhook's menu and click 'Send test'. The result appears straight away and in the 'Delivery log'.
Only leads captured after you add the webhook are sent; earlier leads stay in KnowStack. You can add several webhooks, and each new lead goes to every active one.
The address must be public: https only, on the standard port, with no username or password in it. Addresses on a private or local network are refused, also when a public name later points to one. Redirects are not followed, so paste the final address.
When requests are sent
| Event | When | What it carries |
|---|---|---|
| lead.created | A chat captures a new lead | The lead and the conversation up to that moment |
| lead.updated | A name, email or phone turns up later in the same lead | The lead's current details and a changed list naming the new fields, without the conversation |
| test | You click 'Send test' | A sample lead with id 0 and a short sample conversation |
Messages written after the lead was created are never sent. A lead.updated request goes out only after lead.created was accepted, and only when a field went from empty to filled.
Request headers
| Header | Value |
|---|---|
| Content-Type | application/json |
| User-Agent | LeadWebhook/1.0 |
| X-Webhook-Event | lead.created, lead.updated or test |
| X-Webhook-Delivery | The delivery id. A retry of the same delivery keeps it, so use it to ignore duplicates |
| X-Webhook-Timestamp | When the request was signed, in Unix seconds |
| X-Webhook-Signature | v1= followed by the hex HMAC-SHA256 of the timestamp, a dot and the raw body, keyed with your signing secret |
Payload (version 1)
The body is JSON in UTF-8, sent without line breaks and shown formatted here. Values in angle brackets stand for your own: the product name is KnowStack, and the address is the one you sign in at. A lead.created request:
{
"version": 1,
"event": "lead.created",
"deliveryId": "4821",
"occurredAt": "2026-01-15T10:03:12.000Z",
"lead": {
"id": 1375,
"name": "Anna Novak",
"email": "[email protected]",
"phone": null,
"channel": "web_chat",
"channelLabel": "Website chat",
"source": "<product name> · Website chat",
"question": "Do you have availability next week?",
"firstPageUrl": "https://example.com/pricing",
"locale": "en",
"capturedAt": "2026-01-15T10:02:40.000Z",
"conversationUrl": "https://<your app address>/dashboard/channels/website-chat?conversation=812"
},
"transcript": [
{ "from": "customer", "text": "Hi, do you have availability next week?", "at": "2026-01-15T10:00:05.000Z" },
{ "from": "ai", "text": "Yes, we do. Could I take your email so the team can confirm a time?", "at": "2026-01-15T10:00:09.000Z" },
{ "from": "customer", "text": "Sure, [email protected]", "at": "2026-01-15T10:02:40.000Z" }
],
"transcriptIncomplete": false
}
| Field | Meaning |
|---|---|
| version | Always 1 in this format. A change that could break your code comes with a new number |
| event, deliveryId | The same values as the X-Webhook-Event and X-Webhook-Delivery headers |
| occurredAt | When the request was built, in ISO 8601 (UTC) |
| lead.id | The lead's id in KnowStack. lead.updated carries the same id, so you can find the record you created |
| lead.name, lead.email, lead.phone | Contact details. Any of them can be null |
| lead.channel | Where the chat took place: web_chat, messenger or instagram today. New values appear as channels are added, so accept any text |
| lead.channelLabel, lead.source | The channel's display name and a ready-made lead source such as "KnowStack · Instagram", in the language of the person who added the webhook |
| lead.question | The question the customer asked when the lead was captured, or null |
| lead.firstPageUrl, lead.locale | The page where a website chat started and the visitor's language. Usually null for other channels |
| lead.capturedAt | When the contact details were left |
| lead.conversationUrl | A link to the conversation in KnowStack, for people who can sign in to your company |
| transcript | The conversation up to the moment the lead was created, oldest first, at most 500 messages. Each has from (customer, ai or team), text and at |
| transcriptIncomplete | true when older messages were already deleted under your retention settings. Those messages have text null and purged true |
A lead.updated body has the same version, event, deliveryId, occurredAt and lead fields, with the lead's current values. Instead of the conversation it carries changed, the list of fields filled in since the last request, for example ["phone"].
Verify the signature
- Read the raw request body before parsing it as JSON.
- Join the X-Webhook-Timestamp value, a dot and the raw body.
- Compute the HMAC-SHA256 of that text with your signing secret, the whole value including whsec_, and write it in hex.
- Compare v1= plus that hex with X-Webhook-Signature, using a constant-time comparison.
- Reject the request if the timestamp is more than 5 minutes away from your clock. This stops an old request from being replayed.
An example for Node.js with Express:
const crypto = require('crypto');
const express = require('express');
const SECRET = process.env.LEAD_WEBHOOK_SECRET; // whsec_...
const app = express();
// Keep the body raw: the signature covers the exact bytes that were sent.
app.post('/leads', express.raw({ type: 'application/json' }), (req, res) => {
const timestamp = req.get('X-Webhook-Timestamp') || '';
const received = req.get('X-Webhook-Signature') || '';
const expected = 'v1=' + crypto
.createHmac('sha256', SECRET)
.update(timestamp + '.' + req.body)
.digest('hex');
const fresh = Math.abs(Date.now() / 1000 - Number(timestamp)) <= 300;
const valid = received.length === expected.length &&
crypto.timingSafeEqual(Buffer.from(received), Buffer.from(expected));
if (!fresh || !valid) return res.status(401).end();
const body = JSON.parse(req.body);
// req.get('X-Webhook-Delivery') identifies the delivery: skip it if you
// have already handled it.
console.log(body.event, body.lead.id);
res.status(204).end();
});
app.listen(3000);
To check your code: the secret whsec_test, the timestamp 1761559200 and the body {"a":1} give the signature v1=ed347bcd87edf5b36afc10e41346740621d7f3c26b13ee1a4cc1eb7cddd0786f.
Responses and retries
| Your response | What happens |
|---|---|
| 2xx | Delivered |
| 408, 429, 5xx, a timeout or a network error | Retried at growing intervals for about 24 hours, then marked failed. Retry-After on a 429 is respected, up to an hour |
| 410 | The webhook switches to 'Needs attention' at once |
| 401, 403 or 404 three times with no successful delivery in between | The webhook switches to 'Needs attention' |
| Any other code, including a redirect | Marked failed and not retried |
Answer within 10 seconds, or the attempt counts as a timeout. Only the first 64 KB of your response is read. Respond first and do slow work afterwards.
When a webhook needs attention
While a webhook shows 'Needs attention' or 'Paused', new leads wait in the queue instead of being lost. When a webhook stops, the people who manage users get one alert in the notification bell, and on Telegram if they connected it. Fix the receiving app or correct the address with 'Edit', or click 'Resume' if the address was right. Sending picks up again, starting with the leads that waited.
'Delivery log' in the webhook's menu shows the last 50 requests with their result and response code. Click 'Retry' on a failed one to send it again with the same delivery id. In the 'Leads' tab of each chat channel, a marker shows whether a lead reached your CRM; for a lead that wasn't sent, choose 'Send to CRM again' in its row menu. If leads keep failing while a webhook stays active, its row says 'Recent deliveries failed' and the people who manage users get an alert, at most once a day.
Signing secret
If the secret has leaked, choose 'New signing secret' in the webhook's menu. The old secret stops working at once, so put the new one into the receiving app straight away.
If your company moves to a plan without lead webhooks, new leads are not sent. Leads that come in meanwhile for an active webhook are skipped and are not sent by themselves after an upgrade; once you upgrade, you can send each one with 'Send to CRM again'. Leads waiting on a paused webhook, or on one that needs attention, stay in the queue and go out after you upgrade and resume it. Your webhooks stay listed, and you can still pause or delete them.