Reference for webhook event payload shapes delivered to your server.
Updated November 4, 2026
Loading the reference...
The event examples below describe current full-payload subscriptions. They use the same outer envelope:
Webhook flow: a selected CRM event is posted to your HTTPS receiver with signature, timestamp, and event ID headers. Verify the raw body and signed timestamp before processing, and use the stable event ID to deduplicate retries.
subAccountId - Always present. This is the sub-account ID to use when matching events to a sub-account.
contactId - Present when the event is associated with a specific contact.
tags - Present when the associated contact has tags. Array of tag strings.
Some events also include locationId, an optional public ID for that same sub-account. It appears only when the sub-account has a public ID. Use subAccountId when you need an identifier that is present on every event.
Delivery headers and verification
Every delivery carries these headers:
Header
What it is
X-Webhook-Signature
sha256=<hex>. HMAC-SHA256 of the raw request body, keyed with your subscription's signing secret.
X-Webhook-Signature-V2
sha256=<hex>. HMAC-SHA256 of <timestamp>.<raw body>, same secret. Optional, replay-protected.
X-Webhook-Timestamp
Delivery time in epoch milliseconds. This is the value signed by the V2 signature.
X-Webhook-Event
The event name, matching the event field in the body.
X-Webhook-Delivery
Unique id for this delivery attempt.
X-Webhook-Event-Id
Stable id for the underlying event. Use it to make your handler idempotent, since a retry repeats the same event id.
User-Agent
Carries the sub-account's display brand, so the receiving system sees your brand rather than the platform name.
Which signature to verify
Verify against the raw request body exactly as received, before any JSON parsing or re-serialization, or the bytes will not match.
X-Webhook-Signature (original). Signs the body alone. It proves the payload came from you and has not been altered. It cannot tell you when it was sent, so a captured request stays valid forever and can be replayed at any time.
X-Webhook-Signature-V2 (recommended for new integrations). Signs <timestamp>.<body>, following the same scheme Stripe uses. Because the timestamp is part of the signed string, your handler can trust X-Webhook-Timestamp and reject anything outside a freshness window you choose, which shuts the replay window. Verifying the original signature leaves that timestamp unsigned, so an attacker replaying a captured delivery can rewrite it freely.
Both headers are sent on every delivery, signed with the same secret. Nothing breaks if you keep verifying the original one; move to V2 when it suits you.
Code example
import crypto from "node:crypto";
// `rawBody` must be the exact bytes received, not a re-serialized object.
function verify(rawBody, headers, secret, toleranceMs = 5 * 60 * 1000) {
const timestamp = headers["x-webhook-timestamp"];
const expected = crypto
.createHmac("sha256", secret)
.update(`${timestamp}.${rawBody}`)
.digest("hex");
const received = (headers["x-webhook-signature-v2"] || "").replace("sha256=", "");
const a = Buffer.from(expected, "hex");
const b = Buffer.from(received, "hex");
// Constant-time compare, then reject anything too old to be a live delivery.
if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) return false;
return Math.abs(Date.now() - Number(timestamp)) < toleranceMs;
}
Your signing secret is generated per subscription when you create the webhook, under Settings, Integrations, Webhooks.
The changedFields array lists the names of the fields that changed in this update.
Task cancellation surfaces here.PATCH /api/v1/tasks/{id} with status: "cancelled", and deleting a linked contact (which now cancels the contact's open tasks rather than completing them), both fire task.updated with status: "cancelled" and changedFields: ["status"]. There is no separate task.cancelled event.
Payload shapes may vary slightly depending on the data available at the time of the event. Fields shown as present in examples may be null if the corresponding data was not set. The contactId field is included when the event is associated with a specific contact.