Webhooks
Organizations can register outgoing webhooks so your systems hear about lead activity as it happens. Webhooks are configured in the Lynqu dashboard (Integrations) by admins, and by managers who hold the CRM sync permission, with a target URL, a signing secret, and the events to subscribe to.
Events
Section titled “Events”| Event | Fires when |
|---|---|
lead.created |
A new lead lands in the organization |
lead.updated |
A lead’s details change |
lead.assigned |
A lead gets a new owner |
lead.stage_changed |
A lead moves pipeline stage |
lead.merged |
Two duplicate leads are merged |
contact.scanned |
A card or badge scan becomes a contact |
booking.requested |
Someone asks for a meeting |
automation.rule_fired |
A workflow-automation rule executes |
Delivery
Section titled “Delivery”Each delivery is an HTTPS POST with a JSON body:
{ "event": "lead.stage_changed", "data": { }}and these headers:
| Header | Value |
|---|---|
Content-Type |
application/json |
X-Webhook-Event |
The event name |
X-Webhook-Delivery |
A unique id for this event. Retries of the same event reuse it |
X-Webhook-Timestamp |
When the delivery was signed, in Unix seconds |
X-Webhook-Signature-V2 |
sha256= followed by the hex HMAC-SHA256 of {timestamp}.{raw body}, keyed with your signing secret |
X-Webhook-Signature |
Deprecated. sha256= followed by the hex HMAC-SHA256 of the raw body alone. Still sent for now; verify V2 instead |
Respond with any 2xx within 10 seconds. Redirects are not followed.
Retries and switching off
Section titled “Retries and switching off”- A delivery is attempted up to 3 times, about 30 seconds and then 2 minutes apart.
- Only a
5xx, a429or a network error is retried. Any other status is final. - De-duplicate on
X-Webhook-Delivery: a retry of an event you already processed carries the same id. - After 10 failed deliveries in a row the webhook is switched off and the organization’s admins get an inbox notice. Switching it back on in Integrations resets the count.
- Admins can see the last 50 attempts for each webhook in Integrations, with the status code, duration and error. The log is kept for 30 days and never stores payloads.
Verify the signature
Section titled “Verify the signature”Always verify before trusting a payload. Compute HMAC-SHA256 over the timestamp, a dot, and the raw request body, with your secret, and compare constant-time. Then reject a timestamp older than a few minutes, so a captured delivery cannot be replayed later.
import { createHmac, timingSafeEqual } from "node:crypto";
function isValid(rawBody, headers, secret, maxAgeSeconds = 300) { const timestamp = headers["x-webhook-timestamp"]; const signature = headers["x-webhook-signature-v2"] ?? ""; const expected = "sha256=" + createHmac("sha256", secret).update(`${timestamp}.${rawBody}`).digest("hex"); const fresh = Math.abs(Date.now() / 1000 - Number(timestamp)) <= maxAgeSeconds; const a = Buffer.from(expected); const b = Buffer.from(signature); return fresh && a.length === b.length && timingSafeEqual(a, b);}import hashlib, hmac, time
def is_valid(raw_body: bytes, headers: dict, secret: str, max_age_seconds: int = 300) -> bool: timestamp = headers.get("X-Webhook-Timestamp", "") signature = headers.get("X-Webhook-Signature-V2", "") signed = timestamp.encode() + b"." + raw_body expected = "sha256=" + hmac.new(secret.encode(), signed, hashlib.sha256).hexdigest() fresh = timestamp.isdigit() and abs(time.time() - int(timestamp)) <= max_age_seconds return fresh and hmac.compare_digest(expected.encode(), signature.encode())Reject anything that doesn’t verify. Treat webhook payloads as notifications. When in doubt, re-fetch the resource from the REST API using the ids in the payload.

