Skip to content

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.

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

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.

  • A delivery is attempted up to 3 times, about 30 seconds and then 2 minutes apart.
  • Only a 5xx, a 429 or 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.

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.