Webhooks
Receive real-time notifications when contacts, tickets, or AI actions change in your ChatDrift team.
Webhooks
ChatDrift sends an HTTP POST request to a URL you choose whenever something significant happens in your team — a new contact is created, a ticket is updated, or an AI action is executed. You can use webhooks to keep external systems in sync, trigger your own automation, or pipe data into tools like Zapier.
Set up a webhook
- In your ChatDrift dashboard, go to Settings → Webhooks.
- In Webhook Subscriptions, enter the Endpoint URL, an optional Description, and select the Events you want to receive.
- Leave Enabled on (or turn it off to create the subscription paused).
- Click Create Subscription. ChatDrift displays your signing secret once — copy it now.
After saving, each subscription has these actions:
- Send Test — send a sample event to your endpoint.
- Recent Deliveries — view the latest delivery attempts ChatDrift has made.
- Rotate Secret — generate a new signing secret. The old secret stops working immediately.
- Delete — remove the subscription.
Event types
| Event | When it fires |
|---|---|
contacts.created | A contact is created (from the widget, dashboard, or API) |
contacts.updated | A contact's fields are changed |
tickets.created | A support ticket is created |
tickets.updated | A ticket's fields, status, or priority change |
actions.collect-lead.executed | An AI agent's "collect lead" action runs |
actions.create-ticket.executed | An AI agent's "create ticket" action runs |
actions.schedule-meeting.executed | An AI agent's "schedule meeting" action runs |
actions.send-email.executed | An AI agent's "send email" action runs |
You can subscribe to any combination of events on a single webhook endpoint.
Note
The subscription form in the dashboard currently offers the contacts.created and contacts.updated events.
Delivery
Request shape
Every webhook request is an HTTP POST with the following headers:
| Header | Value |
|---|---|
Content-Type | application/json |
x-nexvio-signature | t={timestamp},v1={hex_signature} |
x-nexvio-timestamp | Unix seconds (same value as t in the signature header) |
x-nexvio-event-id | Unique delivery ID (UUID) |
x-nexvio-event-type | Event name (e.g. contacts.created) |
x-nexvio-delivery-attempt | 1, 2, 3, or 4 |
User-Agent | nexvio-webhooks/1.0 |
Payload envelope
The request body is JSON with this shape:
{
"eventId": "evt_uuid",
"eventType": "contacts.created",
"version": "2026-01-01",
"occurredAt": "2026-06-09T08:00:00.000Z",
"teamId": "team_xxx",
"agentId": "agent_xxx",
"source": "widget-frame",
"payload": { ... }
}
| Field | Type | Description |
|---|---|---|
eventId | string | Unique ID for this event. Use for deduplication. |
eventType | string | The event that fired. |
version | string | Payload version — currently "2026-01-01". |
occurredAt | ISO 8601 | When the event occurred. |
teamId | string | Your ChatDrift team ID. |
agentId | string | null | The AI agent involved, if applicable. |
source | string | "dashboard" or "widget-frame". |
payload | object | Event-specific data — see payloads below. |
Payloads
contacts.created / contacts.updated
{
"id": "ctc_abc",
"team_id": "team_xxx",
"first_name": "Jane",
"last_name": "Smith",
"email": "jane@example.com",
"phone": "+1-555-0100",
"company_name": "Acme Corp",
"website": "https://acme.example.com",
"type": "lead",
"source": "widget",
"tags": "vip, trial",
"address1": "123 Main St",
"city": "Springfield",
"state": "IL",
"country": "US",
"postal_code": "62701",
"custom_fields": "{\"plan\": \"pro\"}",
"is_deleted": false,
"created_at": "2026-06-09T08:00:00.000Z",
"updated_at": "2026-06-09T08:00:00.000Z"
}
Note
tags is a comma-separated string for Zapier compatibility. custom_fields is a JSON string or null.
tickets.created / tickets.updated
{
"id": "tkt_001",
"team_id": "team_xxx",
"subject": "Cannot log in",
"description": "<p>I keep getting an error…</p>",
"description_text": "I keep getting an error…",
"status": "open",
"priority": "high",
"source": "widget",
"type": "question",
"requester_id": "ctc_xyz",
"requester_name": "Jane Smith",
"requester_email": "jane@example.com",
"tags": "login, urgent",
"spam": false,
"due_by": 1781110800,
"fr_due_by": 1781006400,
"created_at": "2026-06-09T07:00:00.000Z",
"updated_at": "2026-06-09T07:05:00.000Z",
"attachment_count": 0,
"contact": {
"id": "ctc_xyz",
"name": "Jane Smith",
"email": "jane@example.com",
"phone": "+1-555-0100",
"company_name": "Acme Corp"
}
}
due_by and fr_due_by are Unix timestamps in seconds (or null); created_at and updated_at are ISO 8601 strings.
tickets.updated additionally includes:
{
"updated_fields": ["status", "priority"],
"previous_values": {
"status": "pending",
"priority": "medium"
}
}
actions.*.executed
{
"id": "action_exec_uuid",
"action_type": "collect-lead",
"action_name": "Collect Lead",
"team_id": "team_xxx",
"agent_id": "agent_xxx",
"agent_name": "AI Agent",
"session_id": "sess_abc",
"contact_id": "ctc_xyz",
"success": true,
"error_message": null,
"executed_at": "2026-06-09T09:00:00.000Z",
"action_input": { "email": "jane@example.com", "name": "Jane Smith" },
"action_output": {
"contact": { "id": "ctc_xyz", "name": "Jane Smith", "email": "jane@example.com" }
},
"conversation_context": {
"session_id": "sess_abc",
"message_count": 5,
"last_user_message": "Sure, my email is jane@example.com",
"last_agent_message": "Thanks! I've saved your details."
}
}
Signature verification
Every request includes an HMAC-SHA256 signature you can use to confirm the payload came from ChatDrift and has not been tampered with.
Algorithm
signature = HMAC-SHA256(signing_secret, "{timestamp}.{raw_body}")
The x-nexvio-signature header value is: t={timestamp},v1={hex_signature}
Verification steps
- Extract
tandv1from thex-nexvio-signatureheader. - Read the raw request body as a string (do not parse it yet).
- Compute
HMAC-SHA256(signing_secret, "{t}.{raw_body}")as a lowercase hex string. - Compare your computed value to
v1using a constant-time comparison. - Optionally reject requests where
tis more than 5 minutes from current time to prevent replay attacks.
Code examples
import { createHmac, timingSafeEqual } from "crypto"
function verifyChatDriftWebhook(
rawBody: string,
signatureHeader: string,
secret: string,
): boolean {
const parts = Object.fromEntries(
signatureHeader.split(",").map((p) => p.split("=")),
)
const timestamp = parts["t"]
const expected = parts["v1"]
if (!timestamp || !expected) return false
const computed = createHmac("sha256", secret)
.update(`${timestamp}.${rawBody}`)
.digest("hex")
const a = Buffer.from(computed)
const b = Buffer.from(expected)
if (a.length !== b.length) return false
return timingSafeEqual(a, b)
}
// Express example
app.post("/webhook", express.raw({ type: "application/json" }), (req, res) => {
const valid = verifyChatDriftWebhook(
req.body.toString(),
req.headers["x-nexvio-signature"] as string,
process.env.CHATDRIFT_WEBHOOK_SECRET!,
)
if (!valid) return res.status(401).send("Invalid signature")
const event = JSON.parse(req.body.toString())
console.log(event.eventType, event.payload)
res.sendStatus(200)
})
Retries
If your endpoint returns a non-2xx response, or the connection times out, ChatDrift retries the delivery up to 3 additional times (4 attempts total) with exponential backoff:
| Attempt | Delay before retry |
|---|---|
| 1 (initial) | — |
| 2 | ~2 seconds |
| 3 | ~4 seconds |
| 4 | ~8 seconds |
After all retries are exhausted, the delivery is marked as permanently failed. You can view the delivery log in Settings → Webhooks by clicking Recent Deliveries on the subscription.
Idempotency
Because network errors can cause retries even after your server has processed the request, your handler should be idempotent. Use the eventId field in the envelope to deduplicate events.
Delivery logs
For each webhook subscription you can view the most recent delivery attempts in the dashboard. Go to Settings → Webhooks and click Recent Deliveries on the subscription.
Each log entry shows:
- Status (
successorfailed), event type, and attempt number - HTTP response status code
- Latency in milliseconds
- Timestamp of the attempt
- If failed: the error message
Testing
Use the Send Test button on any webhook subscription to send a sample event to your endpoint. The test is sent as the first event type the subscription listens to and always uses a synthetic contact payload, so no real data is created.