Filters

Filters decide whether an event routed through a connection should continue to destination delivery.

Use filters when one source receives many webhook event types but only some of those events should reach a specific destination. A filtered-out event is not delivered to that destination. It is recorded as an ignored event for that connection, so operators can tell the event was intentionally skipped instead of failed.

Filters are configured inside a connection's rules array.

JSON
{
  "type": "filter",
  "headers": {
    "x-github-event": "push"
  }
}

Dashboard Editor

In the dashboard, open a connection and turn on the Filter rule in the connection rules panel. The filter editor has four tabs:

  • Headers
  • Body
  • Query
  • Path

Each tab is a JSON editor. You do not need to fill every tab. Add JSON only to the parts of the request you want to match, then save the connection.

For example, the Headers tab can match a provider header:

JSON
{
  "x-source": "shopify"
}

The Body tab can match fields from the JSON payload:

JSON
{
  "type": "order.failed"
}

The Query tab can match parsed query parameters:

JSON
{
  "shop": {
    "contains": "demo"
  }
}

The Path tab also expects valid JSON. For a simple exact path match, enter a JSON string:

JSON
"/orders"

The editor saves only tabs that contain text. Empty tabs are ignored. If all four tabs are empty, the connection cannot be saved with the filter enabled.

The editor accepts operator names with or without $. These two examples are equivalent:

JSON
{
  "shop": {
    "contains": "demo"
  }
}
JSON
{
  "shop": {
    "$contains": "demo"
  }
}

Example: Use All Four Filter Tabs

You can combine all four targets in one filter. FastHook will deliver the event only when Headers, Body, Query, and Path all match.

In the Headers tab:

JSON
{
  "x-source": "shopify"
}

In the Body tab:

JSON
{
  "type": "order.paid",
  "data": {
    "total": {
      "gte": 100
    }
  }
}

In the Query tab:

JSON
{
  "shop": {
    "contains": "demo"
  }
}

In the Path tab:

JSON
"/orders"

The saved connection rule is equivalent to:

JSON
{
  "type": "filter",
  "headers": {
    "x-source": "shopify"
  },
  "body": {
    "type": "order.paid",
    "data": {
      "total": {
        "gte": 100
      }
    }
  },
  "query": {
    "shop": {
      "contains": "demo"
    }
  },
  "path": "/orders"
}

Where Filters Run

Connection rules are normalized when you create or update a connection. Filter rules are part of the connection processing stage, after the event has been accepted by the source and fanned out to the connection.

If a filter does not match:

  • the destination is not called
  • the event is marked ignored for that connection
  • the ignored reason is filter
  • delivery retry rules do not run, because there was no delivery attempt

Filter skips are intentional routing decisions. They should not be treated as failed deliveries.

Supported Targets

A filter can inspect four parts of the incoming webhook event.

TargetWhat it matches
headersInbound request headers after FastHook removes internal control headers.
bodyParsed JSON body, or the raw body string when the payload is not JSON.
queryQuery string parsed into an object.
pathRequest path as a string, such as /stripe/webhook.

A filter rule must include at least one of headers, body, query, or path.

JSON
{
  "type": "filter",
  "body": {
    "type": "invoice.paid"
  }
}

Matching Defaults

Filters are strict by default:

  • multiple targets in the same filter are combined with AND
  • multiple fields inside the same object are combined with AND
  • multiple filter rules on the same connection are combined with AND
  • nested objects match recursively
  • plain values use deep equality

This filter requires both the header and body condition to match.

JSON
{
  "type": "filter",
  "headers": {
    "x-github-event": "push"
  },
  "body": {
    "ref": "refs/heads/main"
  }
}

To express alternatives, use $or.

JSON
{
  "type": "filter",
  "body": {
    "$or": [
      { "type": "invoice.paid" },
      { "type": "payment_intent.succeeded" }
    ]
  }
}

Headers

Header names should be written in lowercase. Runtime platforms commonly normalize incoming headers to lowercase, so this is the most reliable form.

JSON
{
  "type": "filter",
  "headers": {
    "x-github-event": "pull_request"
  }
}

Match by user agent with a case-insensitive regex:

JSON
{
  "type": "filter",
  "headers": {
    "user-agent": {
      "$regex": "github-hookshot",
      "$options": "i"
    }
  }
}

Body

For JSON payloads, body filters can match nested fields.

JSON
{
  "type": "filter",
  "body": {
    "data": {
      "object": {
        "amount": {
          "$gte": 1000
        }
      }
    }
  }
}

For non-JSON payloads, the body is treated as a string. Use string operators such as $contains, $starts_with, $ends_with, or $regex.

JSON
{
  "type": "filter",
  "body": {
    "$contains": "order.created"
  }
}

Query

Query strings are parsed into objects. A single query value becomes a string.

Request:

Text
/webhook?mode=live

Filter:

JSON
{
  "type": "filter",
  "query": {
    "mode": "live"
  }
}

Repeated query parameters become arrays.

Request:

Text
/webhook?tag=orders&tag=priority

Filter:

JSON
{
  "type": "filter",
  "query": {
    "tag": {
      "$contains": "priority"
    }
  }
}

Path

The path target matches the full request path string.

JSON
{
  "type": "filter",
  "path": {
    "$starts_with": "/stripe"
  }
}

Exact path match:

JSON
{
  "type": "filter",
  "path": "/stripe/webhook"
}

Operators

Operator names can be written with or without $. For clarity, use the $ form in configuration.

OperatorMeaning
$eqActual value equals expected value.
$neActual value does not equal expected value.
$gtActual value is greater than expected value.
$gteActual value is greater than or equal to expected value.
$ltActual value is less than expected value.
$lteActual value is less than or equal to expected value.
$inActual value equals one item in the expected array.
$ninActual value equals none of the items in the expected array.
$containsString contains substring, or array contains value.
$not_containsString does not contain substring, or array does not contain value.
$existsField presence check.
$regexString matches a regular expression.
$starts_withString starts with expected string.
$ends_withString ends with expected string.
$i_containsCase-insensitive string contains.
$i_starts_withCase-insensitive string starts with.
$i_ends_withCase-insensitive string ends with.
$andAll child conditions must match.
$orAt least one child condition must match.
$notChild condition must not match.
$optionsRegular expression flags used with $regex.

Equality

Plain values are treated like $eq.

JSON
{
  "type": "filter",
  "body": {
    "type": "customer.created"
  }
}

The explicit form is equivalent:

JSON
{
  "type": "filter",
  "body": {
    "type": {
      "$eq": "customer.created"
    }
  }
}

Objects are compared deeply. Object key order does not matter.

JSON
{
  "type": "filter",
  "body": {
    "metadata": {
      "$eq": {
        "plan": "pro",
        "region": "eu"
      }
    }
  }
}

Numeric and Date Comparisons

Comparison operators support finite numbers, numeric strings, and date strings.

Match amounts greater than 5000:

JSON
{
  "type": "filter",
  "body": {
    "amount": {
      "$gt": 5000
    }
  }
}

Match events created after a timestamp:

JSON
{
  "type": "filter",
  "body": {
    "created_at": {
      "$gte": "2026-05-01T00:00:00.000Z"
    }
  }
}

If either side cannot be compared as a number, date, or string, comparison returns false.

Inclusion

Use $in when a field may equal one of several exact values.

JSON
{
  "type": "filter",
  "body": {
    "type": {
      "$in": ["order.created", "order.paid", "order.refunded"]
    }
  }
}

Use $nin to exclude a set of exact values.

JSON
{
  "type": "filter",
  "body": {
    "environment": {
      "$nin": ["test", "sandbox"]
    }
  }
}

Contains

For strings, $contains checks for a substring.

JSON
{
  "type": "filter",
  "body": {
    "description": {
      "$contains": "urgent"
    }
  }
}

For arrays, $contains checks whether the array contains the expected value.

JSON
{
  "type": "filter",
  "body": {
    "tags": {
      "$contains": "priority"
    }
  }
}

Use the case-insensitive variants for string matching:

JSON
{
  "type": "filter",
  "body": {
    "description": {
      "$i_contains": "urgent"
    }
  }
}

Exists

$exists checks whether a field is present.

JSON
{
  "type": "filter",
  "body": {
    "customer_id": {
      "$exists": true
    }
  }
}

Require a field to be absent:

JSON
{
  "type": "filter",
  "body": {
    "test": {
      "$exists": false
    }
  }
}

A field with value null still exists. $exists only checks whether the field is present or missing.

Regex

$regex only matches strings.

JSON
{
  "type": "filter",
  "headers": {
    "user-agent": {
      "$regex": "Stripe/.*"
    }
  }
}

Use $options for regular expression flags.

JSON
{
  "type": "filter",
  "headers": {
    "user-agent": {
      "$regex": "stripe",
      "$options": "i"
    }
  }
}

Logical Operators

$and requires every condition to match.

JSON
{
  "type": "filter",
  "body": {
    "$and": [
      { "livemode": true },
      { "type": { "$starts_with": "invoice." } }
    ]
  }
}

$or requires at least one condition to match.

JSON
{
  "type": "filter",
  "body": {
    "$or": [
      { "type": "checkout.session.completed" },
      { "type": "payment_intent.succeeded" }
    ]
  }
}

$not inverts a condition.

JSON
{
  "type": "filter",
  "body": {
    "type": {
      "$not": {
        "$starts_with": "customer."
      }
    }
  }
}

Common Recipes

Deliver only GitHub pushes to main

JSON
{
  "type": "filter",
  "headers": {
    "x-github-event": "push"
  },
  "body": {
    "ref": "refs/heads/main"
  }
}

Deliver only production Stripe events

JSON
{
  "type": "filter",
  "body": {
    "livemode": true,
    "type": {
      "$in": ["invoice.paid", "invoice.payment_failed"]
    }
  }
}

Ignore sandbox traffic

JSON
{
  "type": "filter",
  "body": {
    "environment": {
      "$ne": "sandbox"
    }
  }
}

Route only requests with a query flag

JSON
{
  "type": "filter",
  "query": {
    "deliver": "true"
  }
}

Match multiple paths

JSON
{
  "type": "filter",
  "path": {
    "$regex": "^/(stripe|github)/webhook$"
  }
}

Require a nested customer email

JSON
{
  "type": "filter",
  "body": {
    "data": {
      "object": {
        "customer_email": {
          "$exists": true
        }
      }
    }
  }
}

Configure Filters With The API

Create a connection with a filter rule:

Shell
curl -X POST "/v1/connections" \
  -H "Authorization: Bearer fhp_YOUR_PROJECT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "github main pushes",
    "source_id": "src_...",
    "destination_id": "des_...",
    "rules": [
      {
        "type": "filter",
        "headers": {
          "x-github-event": "push"
        },
        "body": {
          "ref": "refs/heads/main"
        }
      }
    ]
  }'

Update an existing connection:

Shell
curl -X PATCH "/v1/connections/web_..." \
  -H "Authorization: Bearer fhp_YOUR_PROJECT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "rules": [
      {
        "type": "filter",
        "body": {
          "type": {
            "$in": ["invoice.paid", "invoice.payment_failed"]
          }
        }
      }
    ]
  }'

Design Tips

  • Start with event type, path, or provider header filters before adding deep body conditions.
  • Prefer allow-list filters such as $in over long chains of exclusions.
  • Keep each destination's filter focused on what that destination actually needs.
  • Use lowercase header names.
  • Test with representative payloads before using a filter on production traffic.
  • When a destination did not receive an event, check whether the connection event was ignored with reason filter before debugging delivery failures.

Edge Cases

If a target is set to null, it is ignored during rule normalization. At least one target must remain.

If an operator object contains an unknown operator, the condition does not match.

If a field is missing and the operator is not $exists, the condition does not match.

If your payload has a field named like an operator, such as eq or in, prefer wrapping the whole object with $eq to avoid ambiguity.

JSON
{
  "type": "filter",
  "body": {
    "payload": {
      "$eq": {
        "eq": "literal-value"
      }
    }
  }
}

Filters run against the original incoming event data available to the connection worker. Do not rely on a transformation rule to create fields that a filter rule then matches.