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 filter object.
  • 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 filter or set it to null, 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:

KeyDescription
bodySchema 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 delivery
  • data – 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 null or missing → deliver all events.
  • Filter is {} (empty object) → deliver all events.
  • Filter has no body key (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.event is "message_template.created" and body.data.created_by_id is 2, 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

OperatorValue typeMeaning
$eqanyField (or whole payload when at top level) must equal this value.
$neqanyField must not equal this value.
$andarray of schemasAll schemas in the array must match the same payload/object.
$orarray of schemas or primitivesAt least one schema or value must match.
$notschema objectThe given schema mustnotmatch.

Examples:

  • $or at top level – payload matches if it matches any of the listed schemas:
{
  "body": {
    "$or": [
      { "hello": "world" },
      { "hello": "mark" }
    ]
  }
}
  • $or on 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 $not and $or to 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

OperatorValue typeMeaning
$existbooleantrue→ field must be present;false→ field must be absent.
{
  "body": {
    "inventory": { "$exist": true }
  }
}
{
  "body": {
    "old_inventory": { "$exist": false }
  }
}

Reference

OperatorValue typeMeaning
$refstring or arrayCompare 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

OperatorValue typeMeaning
$ltnumber or stringLess than.
$ltenumber or stringLess than or equal.
$gtnumber or stringGreater than.
$gtenumber or stringGreater 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: 5 with $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 filter in the request body when creating or updating an endpoint (e.g. POST /v1/developers/endpoints or PUT /v1/developers/endpoints/:id). The value must be an object; use null to clear the filter.
  • Dashboard / Private API (v2): Send filter inside the endpoint object when creating or updating an endpoint. Use null to clear.

Response (list/show) includes the stored filter so you can confirm what's configured.

Limitations

  • Body only: Only the body key is supported at the top level of the filter. Headers, query, or path are not filterable.
  • Comparisons: $lt, $lte, $gt, $gte require 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.