Maalam Developers

Webhooks

How Maalam tells your system that a unit was held, reserved or sold — and how to trust it.

A unit's status is the one fact only Maalam has: a buyer held it, the hold lapsed, the buyer signed and paid, the sale closed. Webhooks are how that reaches your system without polling.

Register an endpoint under Settings → API, choose the events, and Maalam POSTs to it.

Events

EventWhen
unit.heldA buyer opened the unit and it is held for them (five minutes by default)
unit.releasedA hold or a reservation lapsed or was cancelled; the unit is available again
unit.reservedThe buyer signed and the deposit was confirmed
unit.soldThe sale was closed in the back office
unit.availableA unit was put on sale (from unavailable)
unit.unavailableA unit was taken off sale, or archived
pingThe test button in the back office

Changes your own system makes through the API are echoed too — a PUT that sets a unit unavailable produces unit.unavailable. The payload's previousStatus tells you where it came from.

The request

POST https://your.example/maalam
Content-Type: application/json
User-Agent: Maalam-Webhooks/1
X-Maalam-Event: unit.reserved
X-Maalam-Delivery: 7f3e1c0a-…
X-Maalam-Timestamp: 1790330405
X-Maalam-Signature: sha256=5d41402abc4b2a76b9719d911017c592…
{
  "id": "7f3e1c0a-…",
  "type": "unit.reserved",
  "createdAt": "2026-09-25T10:00:00.000Z",
  "data": {
    "unit": {
      "id": "e0000000-…",
      "code": "A-101",
      "projectSlug": "alasala",
      "status": "reserved",
      "previousStatus": "held"
    },
    "booking": { "reference": "MB-2026-004183" }
  }
}

booking is present on unit.reserved and unit.sold, and null otherwise. The reference is the one the buyer sees on their agreement and receipt.

Answering

Answer 2xx within ten seconds, before doing any work. Anything else — a 5xx, a timeout, a refused connection — is retried: eight attempts over about an hour, with the gap doubling from thirty seconds. After the last, the delivery is marked failed and shown as such in the back office, where anyone on your team can send it again.

Redirects are not followed. The signed body is not sent anywhere you did not register.

Verifying the signature

Verify before you act

Anyone who learns your endpoint's URL can post JSON to it. The signature is the only proof a delivery came from Maalam. Reject anything that does not verify, and reject anything whose timestamp is more than five minutes old.

The signature is an HMAC-SHA256 over the timestamp, a dot, and the raw body — the exact bytes you received, not a re-serialised copy — using the secret shown when you registered the endpoint.

import { createHmac, timingSafeEqual } from 'node:crypto';

// `rawBody` must be the bytes as received. In Express: app.use(express.raw({ type: '*/*' })).
export function verify(headers, rawBody, secret) {
  const timestamp = headers['x-maalam-timestamp'];
  const [scheme, given] = String(headers['x-maalam-signature'] ?? '').split('=');
  if (scheme !== 'sha256' || !given || !timestamp) return false;
  if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false;

  const expected = createHmac('sha256', secret)
    .update(`${timestamp}.${rawBody}`)
    .digest('hex');
  return given.length === expected.length &&
    timingSafeEqual(Buffer.from(given), Buffer.from(expected));
}

Idempotency

A retry after a timeout your server actually handled will arrive with the same X-Maalam-Delivery id and the same id in the body. Keep the ids you have processed, and treat a repeat as already done.

The secret

The signing secret is shown once when the endpoint is registered, and again if you rotate it. Rotation takes effect immediately: deliveries already queued sign with the new secret. Store it beside your API key, with the same care.

Testing

Send a test in the back office posts a ping to your endpoint through the same path as a real event — signed, retried, logged. The Log shows every attempt with your server's status code, and lets you send any delivery again.

On this page