Skip to main content
Webhooks push events to your server in real time, so you don’t have to poll. When something happens in the CRM — a record is created, an SMS arrives — we send an HTTP POST to a URL you register. This page is the catalogue of events you can subscribe to. Registering a subscription, signature verification and delivery are covered in the pages linked at the bottom (added as those endpoints ship). You can always fetch this catalogue live:

What an event looks like

Every event has a name and a payload. Names are dot-namespaced and provider-freemessage.sms.received, never anything naming the underlying SMS vendor. That’s deliberate: which providers we use is our operational detail, not part of your integration. The payload carries only the fields listed below. Nothing else is ever included — if an internal system starts recording a new field, it does not silently begin flowing to your endpoint.

The catalogue

Records

Payload: object, record_id, changed_fields (update only), occurred_at.

Messaging

Messaging payloads carry record_id when the message is linked to a record, plus event-specific fields and a timestamp. Sender/recipient are normalised and never carry a provider identifier.

Workflow


Notes for building against this

  • Treat the catalogue as the contract. Fetch GET /v1/webhook-events and map from name; do not hard-code payload field lists you scraped from a sample.
  • New events may be added. Handle an unrecognised event name gracefully (ignore it) rather than erroring — we may publish new events over time.
  • Payloads only grow within their whitelist. A field will not appear that isn’t documented here, but treat missing optional fields as normal.

Verifying signatures

Every delivery carries a Flow9-Signature header. Verify it before trusting the payload — it proves the request came from Flow9 and hasn’t been replayed.
v1 is HMAC-SHA256(secret, t + "." + rawBody), hex-encoded, where secret is the value shown once when you created the subscription and rawBody is the exact bytes of the request body. The timestamp t is inside the signed material, so an old signature can’t be replayed with a new timestamp. Reject anything where t is more than 5 minutes from your clock. Verify against the raw body, before JSON-parsing it. Re-serialising parsed JSON can change bytes and break the check.

Node

Python

Use your platform’s constant-time comparison (crypto.timingSafeEqual, hmac.compare_digest) — a plain == leaks, through timing, how much of the signature matched, which is enough to forge one over many requests.

Filtering deliveries

A subscription can carry a filter so you only receive events that match — for example, only leads from a particular source, or only high-value deals. Filtering happens before delivery: a non-matching event produces no request to your endpoint at all. The filter uses the same grammar as record filtering:
  • Filter fields must be fields the subscribed events actually carry (see each event’s payload above, plus object and record_id). A filter naming an unknown field is rejected with 422 when you create the subscription, not silently ignored later.
  • Operators: eq, neq, gt, gte, lt, lte, contains, in, is_null, combined with and / or nested up to 3 deep.
  • Filters are evaluated as data, never executed — values containing punctuation or code are compared literally.

Delivery log, replay and test events

  • Delivery log shows each attempt: status, attempts, last response code, and a truncated response snippet — enough to debug a failing consumer.
  • Test event enqueues a synthetic delivery with a sample payload for one of the subscription’s events, so you can wire up and verify signature handling before real traffic. It is signed exactly like a real delivery.
  • Replay enqueues a fresh copy of a past delivery (new id, signed anew). Replay and test both require settings:configure; replaying another tenant’s delivery is a 404.