Docs/Developers/Webhooks
DEVELOPERS

Webhooks

Zylar webhook events, how to subscribe an HTTPS endpoint, how to verify the X-Zylar-Signature HMAC-SHA256 header in Node.js and Python, and how retries work.

EARLY ACCESSZylar is rolling out to waitlist members in waves. These docs describe the product as it ships and may change before general availability. Join the waitlist.

Webhooks send Zylar events to your own HTTPS endpoint as they happen: a reply, an interested lead, a bounce, an auto-paused domain. Zylar POSTs a signed JSON payload for each event and retries failed deliveries. This page lists the events, shows how to subscribe and explains how to verify signatures.

NOTE Webhooks are included on every plan. See pricing.

Events

EventWhen it fires
lead.repliedA lead sent any reply.
reply.interestedA reply was tagged Interested.
reply.oooAn out-of-office reply, with the return date.
lead.bouncedA hard or soft bounce.
lead.unsubscribedA lead opted out.
sequence.completedA lead finished all steps of a sequence.
domain.pausedAuto-pause triggered on a domain. See Monitoring and auto-pause.

Subscribe only to the events you need.

Subscribe an endpoint

Create a subscription with POST /webhooks, passing your HTTPS URL and the events you want:

curl -X POST https://api.zylar.io/v1/webhooks \
  -H "Authorization: Bearer zy_live_..." \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://your-app.com/hooks/zylar",
    "events": ["reply.interested", "domain.paused"]
  }'

Response 201 Created:

{ "id": "wh_3n7", "secret": "whsec_..." }

Store the secret securely, for example in an environment variable. You use it to verify that each delivery came from Zylar. API keys are scoped to one workspace, so a subscription receives events from the workspace whose key created it. See API reference.

Payload

Each delivery is an HTTPS POST with a JSON body. An example reply.interested payload:

{
  "event": "reply.interested",
  "lead": {
    "email": "jordan@example.com",
    "first_name": "Jordan",
    "company": "Example Co"
  },
  "sequence": "q4-agencies",
  "step": 2,
  "reply": "Sure, send times for Thursday",
  "inbox": "sender@yourdomain.com"
}

Use the event field to route each delivery to the right handler.

Verify signatures

Every payload is signed. The X-Zylar-Signature header contains an HMAC-SHA256 of the request body, computed with your webhook secret. To verify a delivery:

  1. Read the raw request body, exactly as received. Don't parse and re-serialize the JSON first, or the signature won't match.
  2. Compute HMAC-SHA256 of the raw body using your webhook secret as the key.
  3. Compare your result with the X-Zylar-Signature header using a constant-time comparison.
  4. Reject the request if they don't match.

Constant-time comparison matters: a normal string comparison can leak timing information that helps an attacker forge a signature.

NOTE The examples below compare a hex-encoded digest. Zylar is in early access; if the header format you receive differs, check the webhook details shown in-app.

Node.js

import express from "express";
import crypto from "node:crypto";

const app = express();
const secret = process.env.ZYLAR_WEBHOOK_SECRET;

function isValidSignature(rawBody, signature) {
  if (!signature) return false;
  const expected = crypto
    .createHmac("sha256", secret)
    .update(rawBody)
    .digest("hex");
  const a = Buffer.from(expected, "utf8");
  const b = Buffer.from(signature, "utf8");
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}

// Use the raw body for this route so the bytes match what Zylar signed.
app.post("/hooks/zylar", express.raw({ type: "application/json" }), (req, res) => {
  if (!isValidSignature(req.body, req.get("X-Zylar-Signature"))) {
    return res.status(401).send("invalid signature");
  }
  const event = JSON.parse(req.body.toString("utf8"));
  res.sendStatus(200);
  // Handle the event after acknowledging, e.g. queue it for processing.
  console.log(event.event);
});

app.listen(3000);

crypto.timingSafeEqual throws if the buffers differ in length, so the length check comes first.

Python

import hashlib
import hmac
import json
import os

from flask import Flask, abort, request

app = Flask(__name__)
SECRET = os.environ["ZYLAR_WEBHOOK_SECRET"].encode()


def is_valid_signature(raw_body: bytes, signature: str | None) -> bool:
    if not signature:
        return False
    expected = hmac.new(SECRET, raw_body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, signature)


@app.post("/hooks/zylar")
def zylar_webhook():
    raw_body = request.get_data()  # raw bytes, before any JSON parsing
    if not is_valid_signature(raw_body, request.headers.get("X-Zylar-Signature")):
        abort(401)
    event = json.loads(raw_body)
    # Handle event["event"], ideally by queueing it for async processing.
    return "", 200

Retries

If a delivery fails, Zylar retries it 5 times over 24 hours. To work well with retries:

  • Respond quickly. Acknowledge the delivery first and do slow work (CRM updates, enrichment, notifications) asynchronously.
  • Make handlers idempotent. A retry can deliver an event you've already processed, so make processing the same event twice harmless.
  • Monitor your endpoint. After the retries are exhausted, that delivery is not retried again.

No-code options

The same events work in Zapier and Make. Typical automations:

EventAction
reply.interestedCreate a contact and deal in HubSpot
reply.interestedPost the reply with lead details to a Slack channel
domain.pausedAlert your deliverability channel in Slack

See Integrations.

Cold email, minus the infrastructure. Join the waitlist.

© 2026 ZylarHomeBlogDocsPricingllms.txt