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.
| Field | Type | Description |
|---|---|---|
contactId | string | Stable id for the captured contact. |
email | string | null | Email the visitor entered. |
name | string | null | Name the visitor entered. |
phone | string | null | Phone the visitor entered, if any. |
message | string | null | Free-text message from the lead form. |
visitorId | string | Anonymous visitor id. |
conversationId | string | null | Conversation the lead came from, if any. |
conversationUrl | string | null | Deep link to the conversation in Engine64, if any. |
conversation_started
Fires when a visitor starts a new conversation with the agent.
| Field | Type | Description |
|---|---|---|
conversationId | string | null | The new conversation’s id. |
conversationUrl | string | null | Deep link to the conversation in Engine64, if any. |
visitorId | string | Anonymous visitor id. |
pageUrl | string | null | URL of the page the conversation started on. |
pageTitle | string | null | Title of that page. |
source | string | Where the conversation originated (e.g. "widget"). |
handoff_requested
Fires when a conversation is escalated to a human.
| Field | Type | Description |
|---|---|---|
contactId | string | Stable id for the captured contact. |
email | string | null | Email the visitor entered. |
name | string | null | Name the visitor entered. |
phone | string | null | Phone the visitor entered, if any. |
message | string | null | Free-text message from the lead form. |
visitorId | string | Anonymous visitor id. |
conversationId | string | null | Conversation being escalated, if any. |
conversationUrl | string | null | Deep link to the conversation in Engine64, if any. |
escalatedConversationIds | string[] | Ids of the conversations included in the handoff. |
reason | string | Why 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-256—sha256=<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.