Skip to main content

Webhooks

Webhook subscriptions use a RestHooks-style API. Zapier creates and deletes subscriptions automatically when a user enables or disables a trigger.

Subscribe

POST /api/v1/webhooks/subscribe
Authorization: Bearer lzy_at_your_access_token
Content-Type: application/json
{
"target_url": "https://hooks.zapier.com/hooks/catch/123/abc/",
"event_types": ["lead.created"],
"filters": {
"chatbot_id": "optional-chatbot-id"
}
}

The response returns the subscription and a signing secret. The secret is shown only once.

List and Delete

EndpointMethod
/api/v1/webhooksGET
/api/v1/webhooks/:idGET
/api/v1/webhooks/:idDELETE

All webhook endpoints require the webhooks:manage scope.

Event Types

EventDescription
lead.createdA new lead was captured.
lead.qualifiedA lead reached the qualification threshold.
conversation.message_receivedA new message arrived in a conversation.
ticket.createdA support ticket was created.
meeting.scheduledA meeting was booked.
meeting.cancelledA meeting was cancelled.

Delivery Security

Leezy sends each webhook with these headers:

HeaderPurpose
X-Leezy-SignatureHMAC SHA-256 signature over <timestamp>.<raw_body>, prefixed with sha256=.
X-Leezy-TimestampUnix timestamp (seconds) included in the signed input.
X-Leezy-EventEvent type.
X-Leezy-DeliveryDelivery log ID.

To verify a delivery, recompute the HMAC over ${timestamp}.${raw_body} using the subscription secret and compare with X-Leezy-Signature. The timestamp is part of the signed input — receivers MUST also reject deliveries whose X-Leezy-Timestamp is more than five minutes away from the receiver's current time. Without the timestamp window, a captured payload could be replayed indefinitely because the same body always produces the same signature.

Verification example (Node.js):

import crypto from "node:crypto";

function verifyLeezySignature(rawBody, headers, secret) {
const timestamp = headers["x-leezy-timestamp"];
const signature = headers["x-leezy-signature"];
const ageSeconds = Math.abs(Date.now() / 1000 - Number(timestamp));
if (!timestamp || ageSeconds > 300) return false;

const expected = "sha256=" + crypto
.createHmac("sha256", secret)
.update(`${timestamp}.${rawBody}`)
.digest("hex");

return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expected),
);
}