What an event looks like
Every event has a name and a payload. Names are dot-namespaced and provider-free —message.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-eventsand map fromname; 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 aFlow9-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
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 afilter 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
objectandrecord_id). A filter naming an unknown field is rejected with422when you create the subscription, not silently ignored later. - Operators:
eq,neq,gt,gte,lt,lte,contains,in,is_null, combined withand/ornested 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.
Related
- Working with records — the objects that emit
record.* - Authentication and scopes