Outbound webhooks
Webhooks let Zubby push events to your stack the moment they happen — no polling required. Subscribe an HTTPS endpoint, choose the events you care about, and we’ll deliver signed JSON payloads with automatic retries on failure.
Subscribing
In the dashboard, under Integrations → Webhooks, paste your HTTPS URL and save. Every configured endpoint receives every event listed below. Slack incoming-webhook URLs get a readable text message; Zapier / Make catch-hooks get a flat JSON object; everything else gets the signed envelope described below.
Event catalog
| Event | When it fires |
|---|---|
lead.created | A shopper leaves their email or phone in the widget. |
handoff.requested | A shopper asks for a human from the widget. |
conversation.started | A new shopper conversation begins. |
conversation.escalated | The AI (low confidence) or a teammate escalated a chat. |
conversation.closed | A teammate closed a conversation. |
csat.submitted | A shopper rated their chat experience (1–5 stars). |
ticket.created | A support ticket was opened (any channel). |
recovery.sent | A cart/browse recovery email left our outbox. |
order.refunded | The platform reported an order refund. |
rma.created | The AI logged a shopper return request. |
Payload shape
Every event has the same envelope. The data field differs per event type. id is a unique event id — use it to deduplicate on your side.
{
"id": "5f3c2a1e-9d2b-4c1f-8a4e-1b2c3d4e5f6a",
"event": "lead.created",
"timestamp": "2026-05-15T18:22:43.108Z",
"data": {
"leadId": "lead_01HJKLMN",
"email": "alex@example.com",
"purpose": "lead_capture"
}
}Verifying signatures
Every non-Slack delivery is signed. The X-Zubby-Signature header has the form t=<unix-ts>,v1=<hmac>, where v1 is an HMAC-SHA256 of `${timestamp}.${rawBody}` using your webhook secret (shown when you create the endpoint). Verify before trusting the payload, and reject stale timestamps.
import crypto from "node:crypto";
function verifyZubbyWebhook(req: Request, rawBody: string): boolean {
const header = req.headers.get("X-Zubby-Signature") ?? "";
const match = header.match(/^t=(\d+),v1=([a-f0-9]{64})$/);
if (!match) return false;
const [, timestamp, sig] = match;
// Replay protection: reject anything older than 5 minutes.
if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false;
const expected = crypto
.createHmac("sha256", process.env.ZUBBY_WEBHOOK_SECRET!)
.update(`${timestamp}.${rawBody}`)
.digest("hex");
// Constant-time compare to thwart timing attacks
return crypto.timingSafeEqual(
Buffer.from(sig, "hex"),
Buffer.from(expected, "hex")
);
}Reject anything older than 5 minutes
X-Zubby-Timestamp header. Reject requests where it differs from your server clock by more than 300 seconds — this prevents replay attacks even if a signature leaks.Failures
Deliveries time out after 5 seconds; non-2xx responses count as failures. Deliveries are best-effort (no automatic retry today), and we track consecutive failures per endpoint: after 5 in a row we notify you in the dashboard inbox; after 20 in a row the endpoint is paused automatically. Fix the receiver and re-add it from Integrations → Webhooks.
Best-practice receiver
On the receiving side:
- Verify the signature before parsing.
- Persist the event by
idbefore doing any downstream work — this makes the receiver idempotent. - Return 2xx within 5 seconds. Defer heavy work to a queue (Kafka, SQS, or your DB) — don’t block our delivery worker.
- Log the full envelope for debugging.