Outgoing webhooks
EnableLeads can tell your own system the moment something happens: a contact is created, a task is completed, a deal is won. You give it an https address, it sends a small JSON message there, and you reply with a success code.
Receiving
- Make an https address on your server that accepts POST and answers 200 (any 2xx) within 10 seconds.
- In EnableLeads open Settings, Outgoing Webhooks, Add webhook, paste the address and tick the events you want.
- Copy the signing secret (it starts with
whsec_) and keep it on your server. Use it to check that a message really came from EnableLeads. - Press Send test event. A message of type
webhook.testis sent so you can check your endpoint before anything real happens.
The address must be https, public, and without a username or password in it. Addresses that point at private or internal networks are refused. Redirects are not followed.
Events
Choose any of these for each webhook. A message is sent after the change is saved; imports, bulk changes and merges do not send events.
| Contacts | contact.created A contact is createdcontact.updated A contact's details changecontact.status_changed A contact's status changescontact.assigned A contact is assigned to someone |
| Tasks | task.created A task is createdtask.completed A task is completed |
| Deals | deal.created A deal is createddeal.won A deal is wondeal.lost A deal is lost |
| Activity | call.logged A call is loggednote.added A note is added |
The message
Every event has the same outer shape. The details are under data: a contact, and for task, deal, call and note events also a task, deal, call or note, plus the actor who did it (absent when a lead arrives from a webhook). Deal prices are in rupees.
{
"id": "evt_6f1c1f3e-8a5b-4f6e-9d1a-0c2f2d4b7a10",
"type": "contact.created",
"created_at": "2026-10-26T10:15:30.000Z",
"workspace": {
"id": "4c60db83-0377-4050-8da6-88232e277276",
"name": "Acme Traders"
},
"data": {
"contact": {
"id": "51acac88-f582-4298-939d-7a798addd284",
"url": "https://app.enableleads.com/contacts/51acac88-f582-4298-939d-7a798addd284",
"first_name": "Asha",
"last_name": "Rao",
"designation": "Owner",
"organization": "Sunrise Realty",
"phones": [
"+919830011111"
],
"emails": [
"asha@sunrise.example"
],
"websites": [],
"address": {
"street": "12 Park Street",
"city": "Kolkata",
"state": "West Bengal",
"pincode": "700016",
"country": "India"
},
"status": {
"id": "b1c2d3e4-0000-4000-8000-000000000001",
"name": "New"
},
"source": {
"id": "b1c2d3e4-0000-4000-8000-000000000002",
"name": "Website"
},
"campaign": null,
"tags": [
"Hot lead"
],
"assigned_to": {
"id": "a1b2c3d4-0000-4000-8000-000000000003",
"name": "Tuhin Saha",
"email": "tuhin@acme.example"
},
"owner": {
"id": "a1b2c3d4-0000-4000-8000-000000000004",
"name": "Alok Kumar",
"email": "alok@acme.example"
},
"background": "Enquired about the yearly plan",
"custom_fields": {
"Budget": "5,00,000"
},
"created_at": "2026-10-26T10:15:29.812Z",
"updated_at": "2026-10-26T10:15:29.812Z"
},
"actor": {
"id": "a1b2c3d4-0000-4000-8000-000000000004",
"name": "Alok Kumar",
"email": "alok@acme.example"
}
}
}Headers on every request:
| Content-Type | application/json |
| User-Agent | EnableLeads-Webhooks/1.0 |
| X-EnableLeads-Event | The event name, such as contact.created |
| X-EnableLeads-Delivery | A unique id for this message. A retry of the same message keeps it. |
| X-EnableLeads-Signature | t=<unix seconds>,v1=<signature>: see Checking the signature |
Checking the signature
Anyone can post to a public address, so check every message. The signature is an HMAC-SHA256, in hex, of the timestamp, a full stop, and the raw body exactly as received, using your signing secret as the key. Reject messages with a timestamp more than 5 minutes old, which stops a captured message being replayed. Compare in constant time.
Works in Express, Fastify, Next.js route handlers: just give it the raw text of the body.
import crypto from "node:crypto";
// `rawBody` must be the exact text received, before any JSON parsing.
export function isFromEnableLeads(secret, signatureHeader, rawBody) {
const parts = Object.fromEntries(signatureHeader.split(",").map((p) => p.trim().split("=")));
const timestamp = Number(parts.t);
if (!parts.v1 || Math.abs(Date.now() / 1000 - timestamp) > 300) return false; // too old: a replay
const expected = crypto.createHmac("sha256", secret).update(`${timestamp}.${rawBody}`).digest("hex");
const a = Buffer.from(expected);
const b = Buffer.from(parts.v1);
return a.length === b.length && crypto.timingSafeEqual(a, b);
}
// Express: app.post("/hooks/enableleads", express.raw({ type: "application/json" }), (req, res) => {
// if (!isFromEnableLeads(SECRET, req.get("X-EnableLeads-Signature") ?? "", req.body.toString())) return res.sendStatus(401);
// const event = JSON.parse(req.body.toString());
// res.sendStatus(200); // answer quickly, then do the work
// });Retries
Anything other than a 2xx answer (or no answer in 10 seconds) is retried, with these waits: 1 minute, 5 minutes, 30 minutes, 2 hours, 6 hours. After the sixth failed attempt the message is given up on, and it can be sent again by hand from the delivery log. Retries keep the same X-EnableLeads-Delivery and the same event id.
If 5 messages in a row are given up on, the webhook is paused and the account owner is emailed. Fix the endpoint and press Resume. Longer waits are checked when the account is next active and by the morning job, so a very late retry can be a few hours behind its schedule.
Good practice
- Answer first, work later. Return 200 as soon as the signature checks out, then process the event in the background.
- Expect repeats. A retry can arrive after your first answer was lost. Keep the event
idand ignore one you have already handled. - Do not rely on order. Two quick changes can arrive in either order. The
updated_atinside the contact tells you which is newer. - Keep the secret on your server. If it leaks, press Make a new one next to the signing secret in EnableLeads and update your server.