Webhooks and the rebuild hook
Webhooks
The company subscribes an endpoint to events on its Webhook endpoints screen. Each event is delivered as a POST with a JSON body carrying ids and references only, never personal data: your system reads the record it needs through the API with its own key.
The events
| Family | Events |
|---|---|
| Listings | listing.published, listing.updated, listing.withdrawn |
| Leads and people | lead.created, lead.assigned, lead.status_changed, lead.escalated, contact.created, contact.updated, activity.created, task.created, task.done |
| Bookings | booking.requested, booking.confirmed, booking.paid, booking.cancelled |
| Leases | lease.created, lease.activated, lease.notice_given, lease.ended, lease.updated, rent_payment.received, rent_payment.bounced, rent_payment.overdue |
| Maintenance | job.scheduled, job.done, job.cancelled, job.approval_requested, job.approval_answered, job_charge.booked, job_charge.cancelled |
| Pipelines | deal.stage_changed, deal.won, deal.lost, viewing.planned, viewing.updated, viewing.done, viewing.cancelled, viewing.missed |
Every event carries caused_by: admin, system, website or api:<key prefix>, so a system that writes through the API can ignore the echo of its own writes.
Headers and signature
| Header | Holds |
|---|---|
X-Webhook-Event | The event name, such as lead.created |
X-Webhook-Delivery | The delivery’s id, the same on every retry of one event |
X-Webhook-Signature | t=<unix time>,v1=<hex>: HMAC-SHA256 with the endpoint’s secret over <unix time>.<raw body> |
The secret is shown once, when the company creates the endpoint. Verify the signature over the raw body, before parsing it, and refuse deliveries whose t is more than five minutes old: that closes replays.
import { createHmac, timingSafeEqual } from "node:crypto";
function verify(secret, header, rawBody) {
const parts = Object.fromEntries(header.split(",").map((pair) => pair.split("=")));
const expected = createHmac("sha256", secret).update(`${parts.t}.`).update(rawBody).digest("hex");
const fresh = Math.abs(Date.now() / 1000 - Number(parts.t)) <= 300;
return fresh && expected.length === parts.v1.length && timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1));
}Use X-Webhook-Delivery to drop a delivery you have already handled.
Answer 2xx quickly and do the work afterwards. Deliveries are retried with backoff, and every attempt is listed on the company’s Webhook deliveries screen with its status, so staff can see what your endpoint answered.
Rules for endpoints
- The target must be HTTPS and must not resolve to a private address.
- Deliveries can arrive more than once and out of order: make handling idempotent, and read the record rather than trusting the order of events.
The site rebuild hook
Only for a website built ahead of time, such as a static site generator deployed on a host with a deploy hook. When published listings, areas or team profiles change, Majali sends one POST to the hook address the company sets on its Site rebuild hook screen, debounced for 60 seconds, so a burst of edits causes one build.
- Failures retry with backoff, up to five attempts per batch.
- Changes made while a build is being requested stay pending for the next one.
- The screen shows the last request and its result.
A server-rendered site does not need the hook: it reads the API on each request and caches with ETag.