Webhook Filtering
This document describes the inclusion filters supported for developer webhook endpoints.
Summary
This document describes the inclusion filters supported for developer webhook endpoints. Use these filters so that only events whose payload matches your criteria trigger a webhook delivery. Events that don't match are not delivered (no webhook record or delivery attempt is created).
Overview
- Where to set it: When creating or updating a developer endpoint (Public API or Dashboard/Private API), you can send an optional
filterobject. - What it does: If a filter is set, each event is evaluated against it. Only when the event's webhook body matches the filter is the webhook delivered.
- No filter: If you omit
filteror set it tonull, all events for that endpoint are delivered (no filtering).
Filter shape
The filter must be a JSON object. The only supported top-level key is:
| Key | Description |
|---|---|
body | Schema applied to the webhook request body(the JSON payload that would be sent to your URL). |
Example: Only deliver when the event's data.created_by_id is 2:
{
"body": {
"data": {
"created_by_id": 2
}
}
}All filter rules in this document go under body. The examples below show the full body schema; in the API you wrap that in {"body": { ... } }.
What the webhook body contains
The body your filter is matched against is the same JSON that would be sent in the webhook POST. It typically includes:
api_version– e.g."v1"event– event type string, e.g."message_template.created","lead.new","message.created"request_id– unique ID for this deliverydata– event-specific payload (the resource data; structure depends on event type)
Exact fields depend on the event type. You can inspect a sample delivery or the event's view to see the structure. Filters are evaluated after the body is built and keys are normalized to strings.
Matching rules
The following behavior is implemented and covered by tests.
No filter / empty
- Filter is
nullor missing → deliver all events. - Filter is
{}(empty object) → deliver all events. - Filter has no
bodykey (e.g.{"other": "key"}) → deliver all events.
Simple equality
Match a field to a string, number, boolean, or null:
{
"body": {
"event": "message_template.created",
"data": {
"created_by_id": 2
}
}
}- If the payload's
body.eventis"message_template.created"andbody.data.created_by_idis2, the event matches.
Nested paths
Use nested objects to match nested fields at any depth:
{
"body": {
"product": {
"title": "A product",
"inventory": 0
}
}
}All specified keys must match (implicit and between keys at the same level).
Arrays
Array contains a value
If the payload field is an array and the schema value is a single primitive, the payload matches when that value is present in the array:
{
"body": {
"product": {
"tags": "gift"
}
}
}Matches when body.product.tags is an array that contains "gift".
Array contains all values
If the schema value is an array, the payload array must contain every listed value:
{
"body": {
"product": {
"tags": ["gift", "something"]
}
}
}Matches when body.product.tags contains both "gift" and "something".
Array of objects – any element matches
If the payload field is an array of objects and the schema value is an object, the payload matches if any element of the array matches that schema:
{
"body": {
"order": {
"items": {
"id": 456,
"title": "My product"
}
}
}
}Matches when body.order.items is an array and at least one item has id 456 and title "My product".
Operators
Operators are used as keys in the schema; their value is the operand. They can appear at the top level of body or under any nested key.
Logical
| Operator | Value type | Meaning |
|---|---|---|
$eq | any | Field (or whole payload when at top level) must equal this value. |
$neq | any | Field must not equal this value. |
$and | array of schemas | All schemas in the array must match the same payload/object. |
$or | array of schemas or primitives | At least one schema or value must match. |
$not | schema object | The given schema mustnotmatch. |
Examples:
$orat top level – payload matches if it matches any of the listed schemas:
{
"body": {
"$or": [
{ "hello": "world" },
{ "hello": "mark" }
]
}
}$oron a field – field value can be one of the listed values (or match one of the listed object schemas):
{
"body": {
"product": {
"inventory": { "$or": [1, 5] }
}
}
}$not– deliver when a condition is false, e.g. exclude certain subtypes:
{
"body": {
"$not": {
"event": {
"subtype": { "$or": ["message_changed", "message_deleted"] }
}
}
}
}Matches when body.event.subtype is not "message_changed" or "message_deleted".
- Combine
$notand$orto exclude multiple conditions (e.g. subtype or team):
{
"body": {
"event": { "type": "message" },
"$not": {
"$or": [
{ "event": { "subtype": { "$or": ["message_changed", "message_deleted"] } } },
{ "team_id": { "$or": ["team1", "team2"] } }
]
}
}
}Existence
| Operator | Value type | Meaning |
|---|---|---|
$exist | boolean | true→ field must be present;false→ field must be absent. |
{
"body": {
"inventory": { "$exist": true }
}
}{
"body": {
"old_inventory": { "$exist": false }
}
}Reference
| Operator | Value type | Meaning |
|---|---|---|
$ref | string or array | Compare this field's value to the value at another path in thesame body. String is dot-separated path (e.g."created_at"or"data.updated_at"); array is path segments. |
{
"body": {
"updated_at": { "$ref": "created_at" }
}
}Matches when body.updated_at equals body.created_at.
Comparisons
| Operator | Value type | Meaning |
|---|---|---|
$lt | number or string | Less than. |
$lte | number or string | Less than or equal. |
$gt | number or string | Greater than. |
$gte | number or string | Greater than or equal. |
- Comparisons are supported only when both the payload value and the filter value are numbers, or both are strings. If types differ (e.g. number vs string), the comparison does not match (e.g.
inventory: 5with$lte: "10"does not match).
Examples:
{
"body": {
"product": {
"inventory": { "$lte": 10 }
}
}
}{
"body": {
"count": { "$gt": 2 }
}
}{
"body": {
"status": { "$neq": "archived" }
}
}Real-world examples
Only events created by a specific user
{
"body": {
"data": {
"created_by_id": 2
}
}
}Only form responses belonging to a specific form
{
"body": {
"data": {
"form_id": "46f18a29-04f3-48c5-96f6-74548ee14e20"
}
}
}Only messages with a specific direction
{
"body": {
"data": {
"direction": "OUTBOUND"
}
}
}Only messages of one or more types (e.g. SMS or email)
{
"body": {
"data": {
"type": { "$or": ["sms", "email"] }
}
}
}Only messages that are not outbound
{
"body": {
"$not": {
"data": {
"direction": "OUTBOUND"
}
}
}
}Only non-email, e.g. SMS/WhatsApp)
{
"body": {
"$not": {
"data": {
"type": "email"
}
}
}
}Only when a value is less than (or equal to) a given amount
{
"body": {
"data": {
"position": { "$lte": 5 }
}
}
}Field must match another field (e.g. updated_at equals created_at)
{
"body": {
"updated_at": { "$ref": "created_at" }
}
}API usage
- Public API (v1): Send
filterin the request body when creating or updating an endpoint (e.g.POST /v1/developers/endpointsorPUT /v1/developers/endpoints/:id). The value must be an object; usenullto clear the filter. - Dashboard / Private API (v2): Send
filterinside theendpointobject when creating or updating an endpoint. Usenullto clear.
Response (list/show) includes the stored filter so you can confirm what's configured.
Limitations
- Body only: Only the
bodykey is supported at the top level of the filter. Headers, query, or path are not filterable. - Comparisons:
$lt,$lte,$gt,$gterequire comparable types (number with number, string with string). Mixed types do not match. - Structure: The filter must be a valid JSON object. Invalid or non-object values (e.g. a string) are rejected by the API with a validation error.
Updated about 2 hours ago
