Zinevu
Guides

Webhooks

Be told when something happens, instead of asking every five minutes.

Add an endpoint under Settings → Developers — or with the API — choose the events you want, and we POST JSON to it. HTTPS only, and the URL must resolve to a public address. Five endpoints per account.

Events#

EventFires when
lead.createdA new lead arrived — from a form, Facebook, or created by hand.
lead.status_changedA lead or offer moved to a different status.
offer.sentAn offer was e-mailed to the customer.
offer.signedThe customer signed the offer.
offer.rejectedThe customer declined the offer.
offer.measurement_acceptedThe customer accepted the price and asked to be measured.

What a delivery looks like#

POST https://your-app.example.com/hooks/zinevuX-Zinevu-Event: offer.signedX-Zinevu-Delivery: 9f1c3e2a-...      # stable across retriesX-Zinevu-Timestamp: 1788523200X-Zinevu-Signature: sha256=<hex>{  "id": "9f1c3e2a-...",  "event": "offer.signed",  "created_at": "2026-09-04T13:20:00+02:00",  "data": { "id": 14868, "status": "signed", ... }}

data is the same shape the REST endpoint returns for that record, so a handler can pass it to the code that already reads offers.

Verify every delivery#

The signature is an HMAC-SHA256 over timestamp + "." + rawBody using your endpoint's signing secret. Compare it in constant time, and reject anything whose timestamp is more than a few minutes old — that is what stops a captured request being replayed at you later.

Sign the raw body

Re-serialising the parsed JSON changes the bytes — key order, spacing, unicode escapes — and the signature will not match. Capture the raw body before your framework parses it.

const crypto = require("crypto")function verify(req, secret) {  const timestamp = req.headers["x-zinevu-timestamp"]  const signature = req.headers["x-zinevu-signature"]  // Sign the RAW body. Re-serialising the parsed JSON changes the bytes.  const expected =    "sha256=" +    crypto.createHmac("sha256", secret)          .update(timestamp + "." + req.rawBody)          .digest("hex")  if (signature.length !== expected.length) return false  if (!crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected))) return false  return Math.abs(Date.now() / 1000 - Number(timestamp)) < 300}

Retries and duplicates#

Delivery is at-least-once. Respond 2xx within five seconds — do the work afterwards, not before you answer — and deduplicate on X-Zinevu-Delivery, which stays the same across retries.

  • Failures are retried after 1 minute, 5 minutes, 30 minutes, 2 hours and 6 hours.
  • An endpoint that fails 20 times in a row is switched off automatically, and the portal shows you why.
  • The last 50 deliveries are kept per endpoint, with their response, and any of them can be replayed from the portal.

Testing one#

Point an endpoint at a request-bin style URL first and look at a real delivery before you write the handler. Then switch it to your own URL and make /leads the reconciliation path: if a webhook is ever missed, a nightly updated_since sweep of https://api.zinevu.com/api/public/v1/leads catches it.