Receive changes through webhooks
The owner enables Billing, Integrations, Tasks and Task webhooks under Modules. Install Webhook API using the displayed terms. GET /v1/developer/integrations requires integrations.installations.read; configuration requires independent integrations.webhooks.read and integrations.webhooks.write grants and enabled Project API. PUT …/outbound-webhook accepts endpointUrl, eventTypes and expectedVersion.
Use expectedVersion = null to create; supply the current version to update. Send Idempotency-Key. signingSecret is returned only on the first successful creation or rotation, never on replay. Rotate separately if that response is lost.
Current event catalog
Available events depend on the selected source: inspect eventTypes in the webhook overview. Task webhooks support creation and completion of standalone and client-linked tasks, including API tasks; data.resource distinguishes crm.personal_task from crm.task.
- crm.deal.created.v1
- crm.deal.updated.v1
- crm.deal.stage_changed.v1
- crm.task.created.v1
- crm.task.completed.v1
{
"id": "EVENT_ID",
"type": "crm.deal.created.v1",
"api_version": "2026-07-31",
"created_at": "2026-10-12T12:00:00.000Z",
"data": {
"resource": "crm.deal",
"resource_id": "DEAL_ID"
}
}data contains minimal identifiers, not a customer profile. contact_id and stage_id are optional. A task can have resource = crm.personal_task; a separate public read is not promised. Fetch current deal or CRM task state using the corresponding scoped read.
Signature verification
v1=HMAC-SHA256(secret, timestamp + "." + rawBody)
X-Nova-Webhook-Timestamp— Unix seconds of the attempt, not the event.X-Nova-Webhook-Signature— v1= followed by 64 hex characters. Compare in constant time.X-Nova-Webhook-Id— Delivery ID. Deduplicate using id in JSON.X-Nova-Webhook-Event— Event type; matches type in JSON.
Pass exact bytes before JSON parsing. Reserialization invalidates the signature. The example allows ±300 seconds; synchronize your server clock. Each retry has a new signature and timestamp.
Download the Node.js verification moduleVerification module source
import { createHmac, timingSafeEqual } from "node:crypto";
/** Node 24 receiver helper: supply exact raw bytes and lowercase Node request headers. */
export function verifyNovaWebhook({ body, headers, secret, now = Date.now(), tolerance = 300 }) {
const reject = () => {
throw new Error("Invalid Nova webhook");
};
if (
!(body instanceof Uint8Array) ||
body.byteLength > 256 * 1024 ||
typeof secret !== "string" ||
!secret.startsWith("whsec_") ||
secret.length < 16 ||
!Number.isFinite(now) ||
!Number.isFinite(tolerance) ||
tolerance <= 0 ||
tolerance > 300
)
reject();
const timestamp = headers?.["x-nova-webhook-timestamp"];
const signature = headers?.["x-nova-webhook-signature"];
if (
typeof timestamp !== "string" ||
!/^[1-9][0-9]{0,10}$/.test(timestamp) ||
typeof signature !== "string" ||
!/^v1=[0-9a-f]{64}$/.test(signature) ||
Math.abs(Math.floor(now / 1000) - Number(timestamp)) > tolerance
)
reject();
const expected = createHmac("sha256", secret).update(timestamp).update(".").update(body).digest();
if (!timingSafeEqual(expected, Buffer.from(signature.slice(3), "hex"))) reject();
let event;
try {
event = JSON.parse(Buffer.from(body).toString("utf8"));
} catch {
reject();
}
if (
!event ||
typeof event !== "object" ||
Array.isArray(event) ||
typeof event.id !== "string" ||
!event.id ||
event.id.length > 160 ||
typeof event.type !== "string" ||
!event.type ||
event.type.length > 160 ||
event.api_version !== "2026-07-31" ||
typeof event.created_at !== "string" ||
!Number.isFinite(Date.parse(event.created_at)) ||
!event.data ||
typeof event.data !== "object" ||
Array.isArray(event.data) ||
event.type !== headers?.["x-nova-webhook-event"]
)
reject();
// Durable event-id deduplication and enqueue must be implemented by the receiver.
// Unknown additive event types should be durably acknowledged and skipped safely.
return event;
}
A reliable receiver
- Bound the body, verify signature and envelope. Invalid signature → 401; invalid JSON → 400.
- In one transaction, save the unique event ID and processing job. An already persisted duplicate → 2xx. A failed transaction → 503.
- Return 2xx after commit and process asynchronously. Never acknowledge before durable enqueue.
- Retries and ordering do not ensure once-only business processing. Make handlers idempotent and reconcile current state through the API. Acknowledge and safely skip unknown additive events.
Delivery and recovery
Public HTTPS only, no redirects. 2xx acknowledges receipt; network/timeout/408/425/429/5xx trigger bounded exponential retry. Other responses become dead letters. HTTP transport has a 10-second deadline. After fixing the receiver, use POST …/outbound-webhook/deliveries/{deliveryId}/retry with a new Idempotency-Key.
Pause stops new event capture; paused-period backfill is not promised. Rotation replaces the secret immediately, without overlap. Delivery uses the current endpoint and secret, so replace endpoints only with receivers you control. History is currently limited to recent deliveries; a full replay journal or 72-hour recovery guarantee is unavailable.