ZahrexDevelopersSign in

Webhooks

Zahrex calls your endpoint when something happens. Register endpoints on your app page in Zahrex.

Endpoints must use HTTPS.

Each app can have up to 5 endpoints.

Events we send

lead.created

A new lead was created for your project.

Example payload
{
  "lead_id": "lead_example1",
  "name": "Sara Example",
  "phone": "+212600000000",
  "email": "sara@example.com",
  "stage": "signed_up",
  "subscription": null,
  "project_id": "prj_example1"
}

lead.stage_changed

A lead moved to another stage.

Example payload
{
  "lead_id": "lead_example1",
  "name": "Sara Example",
  "phone": "+212600000000",
  "email": "sara@example.com",
  "stage": "trial",
  "subscription": null,
  "project_id": "prj_example1",
  "from": "signed_up",
  "to": "trial"
}

subscription.started

A lead's subscription started.

Example payload
{
  "lead_id": "lead_example1",
  "name": "Sara Example",
  "phone": "+212600000000",
  "email": "sara@example.com",
  "stage": "paying",
  "subscription": {
    "external_id": "sub_2001",
    "plan": "Pro",
    "status": "active",
    "amount": 29,
    "currency": "USD",
    "interval": "month",
    "started_at": "2026-11-01T09:00:00+00:00",
    "renewed_at": null,
    "canceled_at": null
  },
  "project_id": "prj_example1"
}

subscription.renewed

A lead's subscription renewed.

Example payload
{
  "lead_id": "lead_example1",
  "name": "Sara Example",
  "phone": "+212600000000",
  "email": "sara@example.com",
  "stage": "paying",
  "subscription": {
    "external_id": "sub_2001",
    "plan": "Pro",
    "status": "active",
    "amount": 29,
    "currency": "USD",
    "interval": "month",
    "started_at": "2026-11-01T09:00:00+00:00",
    "renewed_at": "2026-12-01T09:00:00+00:00",
    "canceled_at": null
  },
  "project_id": "prj_example1"
}

subscription.canceled

A lead's subscription was canceled.

Example payload
{
  "lead_id": "lead_example1",
  "name": "Sara Example",
  "phone": "+212600000000",
  "email": "sara@example.com",
  "stage": "churned",
  "subscription": {
    "external_id": "sub_2001",
    "plan": "Pro",
    "status": "canceled",
    "amount": 29,
    "currency": "USD",
    "interval": "month",
    "started_at": "2026-11-01T09:00:00+00:00",
    "renewed_at": null,
    "canceled_at": "2026-12-15T09:00:00+00:00"
  },
  "project_id": "prj_example1"
}

Verify the signature

Every delivery is signed with HMAC-SHA256 using your endpoint secret. The signature is in the Zahrex-Signature header as t=<unix>,v1=<hex>. The signed payload is <t>.<raw body>. Other headers: Zahrex-Event-Id, Zahrex-Delivery-Id.

Always verify against the raw request body, compare signatures in constant time, and reject old timestamps to stop replays.

Node.js
import crypto from "node:crypto"

// Keep the signing secret on your server (env var), never in a repo or browser.
const SECRET = process.env.ZAHREX_WEBHOOK_SECRET
const TOLERANCE_SECONDS = 300 // reject old timestamps (replay protection)

// rawBody must be the exact bytes received, before any JSON parsing.
export function verifyZahrexWebhook(rawBody, headers) {
  const header = headers["zahrex-signature"] ?? ""
  // Header looks like: t=<unix timestamp>,v1=<hex signature>
  const parts = Object.fromEntries(header.split(",").map((p) => p.split("=", 2)))
  const t = Number(parts.t)
  const v1 = parts.v1
  if (!Number.isFinite(t) || !v1) return false

  if (Math.abs(Date.now() / 1000 - t) > TOLERANCE_SECONDS) return false

  const expected = crypto
    .createHmac("sha256", SECRET)
    .update(`${t}.${rawBody}`)
    .digest("hex")

  const a = Buffer.from(expected)
  const b = Buffer.from(v1)
  // Constant-time comparison; lengths must match first or timingSafeEqual throws.
  return a.length === b.length && crypto.timingSafeEqual(a, b)
}
Python
import hashlib
import hmac
import os
import time

# Keep the signing secret on your server (env var), never in a repo or browser.
SECRET = os.environ["ZAHREX_WEBHOOK_SECRET"].encode()
TOLERANCE_SECONDS = 300  # reject old timestamps (replay protection)


def verify_zahrex_webhook(raw_body: bytes, headers: dict) -> bool:
    # raw_body must be the exact bytes received, before any JSON parsing.
    # Header names can arrive in any case, depending on your framework.
    header = {k.lower(): v for k, v in headers.items()}.get("zahrex-signature", "")
    # Header looks like: t=<unix timestamp>,v1=<hex signature>
    parts = dict(p.split("=", 1) for p in header.split(",") if "=" in p)
    try:
        t = int(parts["t"])
        v1 = parts["v1"]
    except (KeyError, ValueError):
        return False

    if abs(time.time() - t) > TOLERANCE_SECONDS:
        return False

    expected = hmac.new(SECRET, f"{t}.".encode() + raw_body, hashlib.sha256).hexdigest()
    # Constant-time comparison.
    return hmac.compare_digest(expected, v1)

Responses, retries and auto-disable

  • Respond with a 2xx within 10 seconds.
  • Failed deliveries are retried after these delays (seconds): 60, 300, 1800, 7200, 21600, 43200.
  • After 20 consecutive failures the endpoint is disabled automatically. Re-enable it from the app page once fixed.
  • Treat deliveries as at-least-once: use the event id to ignore duplicates.