Skip to content

Outbound webhooks

Webhooks let Zubby push events to your stack the moment they happen - no polling required. Subscribe an HTTPS endpoint and we’ll deliver signed JSON payloads, with failure tracking that alerts you when an endpoint keeps erroring. Outbound webhooks are available on the Pro plan and above.

Subscribing

  1. In the dashboard, open Integrations → Webhooks.
  2. Paste your public HTTPS URL, add an optional description, and save. Private, link-local, and cloud-metadata addresses are rejected.
  3. Copy the signing secret shown once after creation - it is not retrievable later. If you lose it, use Rotate signing secret on the endpoint to mint a new one.
  4. Verify the X-Zubby-Signature header on your receiver (code below) before trusting any payload.

Every configured endpoint receives every event listed below - there is no per-event filter. 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

EventWhen it fires
lead.createdA shopper leaves their email or phone in the widget.
handoff.requestedA shopper asks for a human from the widget.
conversation.startedA new shopper conversation begins.
conversation.escalatedThe AI (low confidence) or a teammate escalated a chat.
conversation.closedA teammate closed a conversation.
csat.submittedA shopper rated their chat experience (1–5 stars).
ticket.createdA support ticket was opened (any channel).
recovery.sentA cart/browse recovery email left our outbox.
order.refundedThe platform reported an order refund.
rma.createdThe AI logged a shopper return request.

Event data fields

The fields below come from the current emitters. Optional context may be absent or null. Store IDs and secrets are not part of these examples. Treat shopper messages, contact details and page context as personal data.

EventData fields
lead.created / handoff.requestedleadId, email, name, phone, purpose, shopperMessage, pageContext, conversationId
conversation.startedconversationId, sessionId, pageContext
conversation.escalatedconversationId, triggeredBy; automated paths may add trigger context
conversation.closedconversationId, closedBy
csat.submittedconversationId, rating (1–5), comment
ticket.createdticketId, ticketNumber, source, channel, conversationId
recovery.sentrecoveryMessageId, cartId, recipientEmail, source
order.refundedplatformOrderId, amountCents (integer), currency
rma.createdrmaId, platformOrderId, reason, rmaType, autoApproved, conversationId, shopperEmail, channel, handle

Illustrative refund data: {"platformOrderId":"1042","amountCents":2500,"currency":"USD"} records a platform-reported refund of USD 25.00. It is a notification, not a command to issue another refund. A return request likewise does not prove a refund has happened.

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. Each delivery also carries X-Zubby-Event, X-Zubby-Event-Id, and X-Zubby-Timestamp headers.

json
{
  "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.

js
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

The signed timestamp is also echoed in the X-Zubby-Timestamp header. Reject requests where it differs from your server clock by more than 300 seconds - this limits the replay window. A valid event can still be replayed inside that window, so persist the event ID and reject duplicate processing. Slack payloads are not signed; Make and Zapier receive a flat object whose exact raw body is signed. Verify that body, not a reconstructed envelope.

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.

Failure counters and alerts are best-effort and depend on service availability. There is no merchant replay action or guaranteed backfill. Fix the receiver before re-adding it, install its newly shown secret, and trigger one controlled event. Rotate signing secret replaces the key immediately; coordinate the receiver update because the old key no longer verifies new sends.

For a missed period, compare your receiver records with the relevant dashboard records or exports. The public API can read conversations and leads; it does not expose general order, customer or revenue report reads. Use the source commerce platform for order reconciliation and contact support when the required history is unavailable. API request retries, catalog synchronization, inbound platform webhooks and billing retries are separate mechanisms.

Best-practice receiver

On the receiving side:

  1. Verify the signature before parsing.
  2. Persist the event by id before doing any downstream work - this makes the receiver idempotent.
  3. Return 2xx within 5 seconds. Defer heavy work to a queue (Kafka, SQS, or your DB) - return the acknowledgement before doing heavy work.
  4. Log the event ID, event name, time and redacted error details. Store only the data needed for processing, with access and retention controls.

Verify a receiver without sending to shoppers

Use a test store and an endpoint you control. Create one synthetic lead such as alex@example.com. Confirm one event ID reaches your receiver, the signature passes and a duplicate ID does not run the action twice. Return a controlled non-2xx response for another test event and confirm your own logs record it; do not wait for an automatic retry. If nothing arrives, check Pro access, the active endpoint, public HTTPS reachability and the selected event source.

Adjacent

Missing something, or found an error on this page?

Contact support and include the URL of this page (/docs/webhooks).