All docs

Docs / API & CLI

Webhooks

Register an endpoint in Settings → Webhooks and Amolfi POSTs you a signed JSON body when the business moves. Thirty-one events exist, in twelve families, and every one of them fires from real code — registration rejects any name that does not.

The catalog

deal · contact · client · project    created / updated
invoice                             created / paid
contract                            sent / signed / declined / expired
run                                 waiting_approval / completed / failed / cancelled
booking                             created / updated / cancelled
member                              invited / joined / removed
domain                              verified
apikey                              created / revoked
file                                available / held / updated / archived

Some absences are deliberate, so you do not sit waiting for them. There is no invoice.overdue: overdue is derived at read time from an invoice’s due date and status, so there is no moment to fire on — compute it from the payloads you already get. Imports and backfills never fire events, so nobody floods your endpoint by loading history. And invoice.paid fires only on the terminal transition to fully paid, never on a partial payment.

The envelope

{ "event": "invoice.paid", "timestamp": "<ISO-8601>", "data": { "id": "…" } }

CRM-family payloads carry the entity’s fields. Every other family is an explicit allow-list projection — thin identity and state, chosen field by field, so internals never leave the workspace by accident.

  • invoice — numbers, status, currency, amounts, client, dates. No notes, no metadata.
  • contract — identity and state only. No signer names or emails; fetch detail through the API.
  • run — id, status, title, summary, and the decision id when one is waiting.
  • booking — identity and the time window. No attendees, description, or location.
  • member — ids, status, and role. No email address, in any form — not plaintext, not hashed. Resolve identity with an authenticated roster read, where your own permissions apply.
  • domain — the domain, its purpose, and when it verified.
  • apikey — the key’s public id and status. Never the key, its hash, or any prefix bytes.
  • file — identity, lifecycle status, content type, byte size, rights state, and scan verdict. Never names, descriptions, tags, content, storage coordinates, or scanner threat names.

Delivery is at-least-once

Retries ride a queue, and every attempt is ledgered with a dedupe check taken *before* the POST, so an acknowledged delivery is not re-sent. Even so, treat your handler as idempotent: dedupe on the X-Amolfi-Delivery header for exact retries, and on (event, data.id) if your handler must act strictly once per business moment.

Verifying the signature — read this part carefully

Every delivery carries three headers: X-Amolfi-Event, X-Amolfi-Delivery, and the signature.

X-Amolfi-Signature: sha256=<hex hmac>

The HMAC key is not your raw `whsec_…` secret. Amolfi never stores that secret — you are shown it once and only its SHA-256 is kept — so the key both sides share is the hex digest of your secret, and that is what you must HMAC with. Every subscriber that skips this step sees valid deliveries as forgeries.

signing_key = sha256_hex(raw_whsec_secret)      // the hex digest STRING
expected    = "sha256=" + hmac_sha256_hex(signing_key, raw_request_body)
valid       = timing_safe_equal(expected, X-Amolfi-Signature)

In Node:

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

function verify(rawBody, header, whsecSecret) {
  const signingKey = createHash('sha256').update(whsecSecret).digest('hex');
  const expected = `sha256=${createHmac('sha256', signingKey).update(rawBody).digest('hex')}`;
  const a = Buffer.from(expected);
  const b = Buffer.from(header || '');
  return a.length === b.length && timingSafeEqual(a, b);
}

Three rules that go with it: HMAC the raw request bytes, not an object you re-serialized — re-serializing changes them. Always compare in constant time. And reject anything unsigned or mis-signed; a delivery that fails this check is not from Amolfi.

Read how it actually works. Setup, security, and the model underneath — in plain language, no marketing in the way.