Skip to Content
IntegrationsWebhooks

Webhooks

When an agent event fires (a lead is captured, a conversation starts, a handoff is requested), Engine64 POSTs a JSON envelope to every URL subscribed to that event. Delivery is near-real-time and durable: it runs through a background job (Inngest) that retries with backoff on any non-2xx response, so a brief outage on your side won’t drop the notification.

The envelope

Every delivery has the same top-level shape:

{ "version": "1", "event": "lead_captured", "data": { }, "timestamp": "2026-07-05T10:00:00.000Z" }
  • version — the payload schema version. It bumps only on a breaking change to the envelope. Treat any field you don’t recognize as additive and ignore it; consumers should tolerate unknown fields.
  • event — which event fired (see below).
  • data — the event-specific fields, documented per event below.
  • timestamp — ISO-8601 UTC time the event was emitted.

Events

lead_captured

Fires when a visitor submits their contact details.

FieldTypeDescription
contactIdstringStable id for the captured contact.
emailstring | nullEmail the visitor entered.
namestring | nullName the visitor entered.
phonestring | nullPhone the visitor entered, if any.
messagestring | nullFree-text message from the lead form.
visitorIdstringAnonymous visitor id.
conversationIdstring | nullConversation the lead came from, if any.
conversationUrlstring | nullDeep link to the conversation in Engine64, if any.

conversation_started

Fires when a visitor starts a new conversation with the agent.

FieldTypeDescription
conversationIdstring | nullThe new conversation’s id.
conversationUrlstring | nullDeep link to the conversation in Engine64, if any.
visitorIdstringAnonymous visitor id.
pageUrlstring | nullURL of the page the conversation started on.
pageTitlestring | nullTitle of that page.
sourcestringWhere the conversation originated (e.g. "widget").

handoff_requested

Fires when a conversation is escalated to a human.

FieldTypeDescription
contactIdstringStable id for the captured contact.
emailstring | nullEmail the visitor entered.
namestring | nullName the visitor entered.
phonestring | nullPhone the visitor entered, if any.
messagestring | nullFree-text message from the lead form.
visitorIdstringAnonymous visitor id.
conversationIdstring | nullConversation being escalated, if any.
conversationUrlstring | nullDeep link to the conversation in Engine64, if any.
escalatedConversationIdsstring[]Ids of the conversations included in the handoff.
reasonstringWhy the handoff fired. Currently the constant "lead_form_submitted".

Sample payload

A full lead_captured delivery:

{ "version": "1", "event": "lead_captured", "data": { "contactId": "test-contact", "email": "test@example.com", "name": "Test Lead", "phone": null, "message": "This is a test notification from Engine64.", "visitorId": "test-visitor", "conversationId": "test-conversation", "conversationUrl": "https://app.example.com/o/a/conversations?c=test-conversation" }, "timestamp": "2026-07-05T10:00:00.000Z" }

Signature verification

When a webhook has a secret configured, Engine64 signs each delivery so you can confirm it really came from Engine64 and hasn’t been tampered with or replayed. Two headers are added:

  • x-signature-256sha256=<hex>, an HMAC-SHA256 of the string `${timestamp}.${body}` (the timestamp header value, a literal dot, then the raw request body) keyed by your webhook secret.
  • x-signature-timestamp — the millisecond epoch used in that signing string.

To verify: recompute the HMAC over `${timestamp}.${rawBody}` with your secret, hex-encode it, and compare against the value after the sha256= prefix using a constant-time comparison. Reject the request if the timestamp is more than ~5 minutes from now to prevent replays.

Signing only happens when a secret is set on the webhook; unsecured webhooks arrive without these headers.

import { createHmac, timingSafeEqual } from "node:crypto"; // `rawBody` must be the exact bytes of the request body, before JSON parsing. function verifyEngine64Webhook({ secret, rawBody, signatureHeader, timestampHeader }) { const ts = Number.parseInt(timestampHeader, 10); if (!Number.isFinite(ts)) return false; // Reject replays: timestamp must be within ~5 minutes. if (Math.abs(Date.now() - ts) > 5 * 60 * 1000) return false; const match = /^sha256=([a-f0-9]{64})$/i.exec(String(signatureHeader).trim()); if (!match) return false; const provided = Buffer.from(match[1], "hex"); if (provided.length !== 32) return false; const expected = createHmac("sha256", secret) .update(`${timestampHeader}.${rawBody}`) .digest(); return timingSafeEqual(provided, expected); }

Reliability notes

  • Return a 2xx quickly; deliveries retry on any non-2xx response.
  • Deliveries can arrive more than once — treat handlers as idempotent.
  • Log failed deliveries and alert on sustained failures.
Last updated on
Engine64 | Docs