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.
- Click New Webhook.
- Enter the destination URL and select the events you want to receive.
- Click Create. ChatDrift displays your signing secret once — copy it now.
After saving you can:
- Test the subscription by sending a sample event.
- View delivery logs for every attempt ChatDrift has made.
- Rotate the signing secret at any time. The old secret stops working immediately.
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.
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": "2026-06-10T17:00:00.000Z",
"fr_due_by": "2026-06-09T12:00:00.000Z",
"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"
}
}
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 → [subscription] → Deliveries.
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 a log of recent deliveries in the dashboard:
Settings → Webhooks → [subscription] → Deliveries
Each log entry shows:
- Attempt number and status (
successorfailed) - HTTP response status code
- Latency in milliseconds
- Timestamp of the attempt
- If failed: error message and scheduled retry time
Testing
Use the Send test button on any webhook subscription to trigger a sample contacts.created event to your endpoint. The test uses a synthetic contact payload so no real data is created.