Designing API Webhook Filtering at Send Time: Patterns That Reduce Bandwidth Without Losing Events

Most webhook providers send every event of a subscribed type to every subscriber. Customer-side filtering wastes bandwidth and forces customers to write their own filter logic. Send-time filtering trades server complexity for substantial customer-side savings, and the trade is usually worth it.

The default webhook design is broadcast: when an event happens, the provider looks up every subscription for that event type and sends the event to each. The customer receives every event of every type they are subscribed to and filters on their end. This works, but it scales poorly. A customer subscribed to invoice.created who only cares about invoices over $1000 receives every invoice and discards 99 percent of them on the receiving side, paying for bandwidth, parsing, and database writes for events they will never act on.

Send-time filtering is the alternative. The provider applies customer-specified filter criteria before delivery and sends only events that match. The trade is server-side complexity (filter evaluation, filter management, filter API) for substantial customer-side savings. For B2B SaaS at any meaningful scale, the trade is usually worth making.

What server-side filtering buys

The bandwidth savings are the obvious win. A customer receiving 100,000 events per day and acting on 1 percent of them is paying for and processing 99,000 wasted webhook deliveries. The cost is real: cloud egress, ingress on the customer side, parsing time, database writes for audit logs, and the cognitive load of seeing irrelevant events in dashboards and notification systems.

The reliability gains are less obvious but equally substantial. Webhook deliveries are bursty and the burst rate is what determines receiver capacity planning. If a customer needs to handle 100,000 events per day but only acts on 1,000, their receiver still needs to scale to handle the full rate. Send-time filtering shifts that capacity planning from the customer to the provider, where it can be amortized across all subscribers.

The product surface is the third benefit. Filter criteria become a customer-facing concept that exists in the subscription dashboard, in the API, in documentation. Customers can subscribe to invoice.created where amount > 1000 and currency = 'USD' as a single concept, rather than subscribing to invoice.created and then implementing the filter in their handler code. The cognitive shift is real.

The filter grammar question

The choice of filter grammar is the most consequential design decision. A grammar too restrictive forces customers to fall back to client-side filtering for cases the grammar does not support. A grammar too permissive becomes a security and performance problem. The right point is usually narrower than first instinct suggests.

The minimum useful grammar is field-equals-value: subscribe to invoice.created where currency = 'USD'. This handles the most common filter case and is trivial to implement and reason about. The implementation is a single equality check per event-subscription pair, which is fast and predictable.

The next step up is field-comparison: subscribe to invoice.created where amount > 1000. This adds a small set of comparison operators (>, >=, <, <=, !=) and handles the second-most-common case. Implementation cost remains low.

The third step up is field-in-set: subscribe to invoice.created where currency in ('USD', 'EUR', 'GBP'). This handles the third-most-common case and is straightforward.

Beyond these three, the diminishing-returns curve gets steep. Regular expression matching, complex Boolean combinations, nested field traversal, and arithmetic expressions all add real implementation cost and produce filters that are increasingly hard to reason about. The right default is to start with the three patterns above and let customer demand drive expansion rather than building a complete query language up front.

The filter storage and evaluation question

Filters live in a database table joined to subscriptions. The schema is straightforward: subscription_id, filter_field, filter_operator, filter_value. A subscription with multiple filter clauses is multiple rows, joined by an implicit AND. A subscription with no filter rows matches all events of its event type.

Evaluation happens at delivery time. The pipeline is event-arrives, look up all subscriptions for the event type, evaluate filters for each subscription, deliver to matching subscriptions. The filter evaluation needs to be fast because it runs once per event per matching subscription, and the count grows with the number of subscribers.

For small subscriber counts, in-memory evaluation against a per-event-type cache of subscriptions is fastest. For larger subscriber counts, push the filter evaluation into the database via a query that joins event-fields against the filter table. The crossover point depends on event volume and subscription count but is usually in the thousands of subscribers per event type.

The subscription dashboard surface

Customer-facing filter management is where the design either succeeds or fails. A filter that exists in the database but is hard to discover, modify, or test produces customer frustration and support tickets. The dashboard surface needs three operations: create-filter-on-subscription, view-current-filters, test-filter-against-sample-event.

The test-filter-against-sample-event operation is the underused one. Customers want to see what a filter will and will not match before they commit to it. A test mode where the customer pastes a sample event and sees whether the filter matches converts most filter-misconfiguration support tickets to self-service.

The current-filters view should show the filter in human-readable form, not as raw SQL or JSON. "Amount greater than 1000 AND currency in (USD, EUR, GBP)" is more readable than the equivalent stored representation. The translation from stored to displayed form is part of the filter implementation.

What filter semantics should not change

Three things should remain constant regardless of filters. First, the event still has its full payload when delivered; filters affect whether the event is delivered, not what is in it. Second, the customer-visible event ID is the same as the one the provider would assign without filters; the filtered-out events do not produce gaps in the ID sequence the customer sees. Third, the subscription is still considered to be the same subscription before and after a filter change; the customer does not lose their dashboard history or audit trail when they modify a filter.

The first point matters because customers sometimes want filters as an efficiency optimization rather than a fundamental subset. A customer who wants every event but processes them faster with a filter should not be surprised by missing fields in the payload.

The second point matters because customers use event IDs to detect missed events. If filtering changes the apparent ID sequence, customers cannot tell whether a gap is a filter or a delivery failure.

Three patterns that fail

First, post-delivery filtering: the provider delivers everything and the customer filters. This is the default and the case for moving away from it is the reason this post exists. The implementation cost is zero on the provider side and substantial on every customer side, and the asymmetry is paying back over time.

Second, opaque filter grammars: a filter language that customers find hard to construct and reason about. Some providers have built sophisticated query grammars that are powerful but produce a substantial fraction of support tickets in filter syntax errors. The right default is to err on the side of restriction.

Third, silent filter failure: a filter that the provider could not evaluate (because of an unsupported field, a type mismatch, a syntax error introduced by a schema change) silently fails open and delivers everything, or fails closed and delivers nothing. Either case produces customer surprise. The right behavior is explicit failure with an audit trail and a notification to the customer.

Application across our four products

WebhookVault has the most natural application for send-time filtering because customers can subscribe to specific endpoints they have created and the filtering naturally extends to per-request properties like method, status code, and header values. The implementation is on the roadmap but not yet shipped; the current version delivers all captured requests to subscribed webhooks.

FlagBit emits flag-evaluation events that are extremely high-volume and where customers usually want only a small subset. Filter-on-flag-key and filter-on-evaluation-result are the canonical filters customers ask for, and adding them would reduce delivery volume by approximately 95 percent for typical customers.

CronPing emits monitor-state-change events at lower volume but with similarly skewed customer interest. Filter-on-monitor-id and filter-on-state-transition (paused, resumed, failed) cover most customer needs.

DocuMint has lower webhook volume because invoice generation is customer-initiated, but filters on invoice-amount and customer-id are the canonical asks.

The deeper observation is that send-time filtering is an asymmetric investment: substantial server-side work that pays back across every customer who uses it. The asymmetry is what makes it valuable in B2B SaaS where customer-side engineering time is the binding constraint. The right default in 2026 is to ship the basic three filter patterns with the initial webhook API and expand based on customer demand, rather than treating filtering as a future feature.

Our products: DocuMint (PDF invoice generation API), CronPing (cron job monitoring with status pages), FlagBit (feature flags API for modern teams), and WebhookVault (webhook capture and replay) keep the lights on.

Read more