{"id":1174,"date":"2026-05-24T13:00:00","date_gmt":"2026-05-24T18:00:00","guid":{"rendered":"https:\/\/tolinku.com\/blog\/?p=1174"},"modified":"2026-03-07T03:35:00","modified_gmt":"2026-03-07T08:35:00","slug":"webhook-filtering","status":"publish","type":"post","link":"https:\/\/tolinku.com\/blog\/webhook-filtering\/","title":{"rendered":"Webhook Filtering: Receiving Only Events You Need"},"content":{"rendered":"\n<p class=\"wp-block-paragraph\">A webhook endpoint that receives every event type processes a lot of noise. If you only care about install attribution, you don&#39;t need thousands of <code>link.clicked<\/code> events clogging your receiver. If your Slack integration only notifies on referral completions, it shouldn&#39;t be parsing click payloads just to discard them.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Filtering reduces load, simplifies handler logic, and keeps your integrations focused. This guide covers filtering strategies for <a href=\"https:\/\/tolinku.com\/features\/webhooks\">Tolinku webhooks<\/a> at every layer: source-level event selection, receiver-side routing, and content-based filtering. For the webhook setup, see the <a href=\"https:\/\/tolinku.com\/blog\/webhook-setup-guide\/\">webhook setup guide<\/a>. For payload details, see the <a href=\"https:\/\/tolinku.com\/blog\/webhook-event-types\/\">webhook event types guide<\/a>.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><img decoding=\"async\" src=\"https:\/\/tolinku.com\/blog\/wp-content\/uploads\/2026\/03\/platform-webhooks.png\" alt=\"Tolinku webhook configuration for event notifications\">\n<em>The webhooks page with create form, webhook list, and delivery log.<\/em><\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Layer 1: Filter at the Source<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">The most efficient filter is the one that prevents events from being sent in the first place. When you configure a webhook in your <a href=\"https:\/\/tolinku.com\/docs\/user-guide\/webhooks\/\">Tolinku Appspace<\/a>, you select which event types to subscribe to.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The available event types are:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><code>link.clicked<\/code><\/li>\n<li><code>deferred_link.claimed<\/code><\/li>\n<li><code>install.tracked<\/code><\/li>\n<li><code>referral.created<\/code><\/li>\n<li><code>referral.completed<\/code><\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">If your webhook endpoint only handles referral workflows, subscribe to <code>referral.created<\/code> and <code>referral.completed<\/code>. Tolinku won&#39;t send the other event types to that endpoint.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Multiple Webhooks for Different Purposes<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Instead of one webhook that receives everything and filters in your code, create separate webhooks for each integration:<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table>\n<thead>\n<tr>\n<th>Webhook Name<\/th>\n<th>Events<\/th>\n<th>Endpoint<\/th>\n<\/tr>\n<\/thead>\n<tbody><tr>\n<td>Analytics Pipeline<\/td>\n<td>All events<\/td>\n<td><code>https:\/\/api.example.com\/webhooks\/analytics<\/code><\/td>\n<\/tr>\n<tr>\n<td>Slack Notifications<\/td>\n<td><code>install.tracked<\/code>, <code>referral.completed<\/code><\/td>\n<td><code>https:\/\/api.example.com\/webhooks\/slack<\/code><\/td>\n<\/tr>\n<tr>\n<td>CRM Updates<\/td>\n<td><code>referral.created<\/code>, <code>referral.completed<\/code><\/td>\n<td><code>https:\/\/api.example.com\/webhooks\/crm<\/code><\/td>\n<\/tr>\n<tr>\n<td>Email Triggers<\/td>\n<td><code>install.tracked<\/code>, <code>referral.completed<\/code><\/td>\n<td><code>https:\/\/api.example.com\/webhooks\/email<\/code><\/td>\n<\/tr>\n<\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">Each endpoint receives only the events it needs. No wasted bandwidth, no wasted processing.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Tolinku supports up to 50 webhooks per Appspace on paid plans (1 on the free plan), so you have room for purpose-specific endpoints.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Layer 2: Filter in the Receiver<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Even with source-level filtering, you may want finer-grained control. The <code>X-Webhook-Event<\/code> header lets you route events before parsing the body.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Header-Based Routing<\/h3>\n\n\n\n<pre><code class=\"language-typescript\">app.post(&#39;\/webhooks\/tolinku&#39;, (req, res) =&gt; {\n  \/\/ Verify signature...\n  res.status(200).send(&#39;OK&#39;);\n\n  const eventType = req.headers[&#39;x-webhook-event&#39;] as string;\n\n  switch (eventType) {\n    case &#39;install.tracked&#39;:\n      handleInstall(req.body);\n      break;\n    case &#39;referral.completed&#39;:\n      handleReferralCompletion(req.body);\n      break;\n    default:\n      \/\/ Ignore other event types\n      break;\n  }\n});\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">This is faster than parsing the JSON body and checking <code>event.event<\/code>, though the difference is negligible at low volumes.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Allow-List Pattern<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Define the events your receiver handles and reject (silently) anything else:<\/p>\n\n\n\n<pre><code class=\"language-typescript\">const HANDLED_EVENTS = new Set([\n  &#39;install.tracked&#39;,\n  &#39;referral.created&#39;,\n  &#39;referral.completed&#39;,\n]);\n\napp.post(&#39;\/webhooks\/tolinku&#39;, (req, res) =&gt; {\n  \/\/ Verify signature...\n  res.status(200).send(&#39;OK&#39;);\n\n  const eventType = req.headers[&#39;x-webhook-event&#39;] as string;\n  if (!HANDLED_EVENTS.has(eventType)) return;\n\n  const event = JSON.parse(req.body.toString());\n  processEvent(event);\n});\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Always respond 200 even for filtered events. A non-200 response triggers retries, wasting resources on both sides.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Layer 3: Content-Based Filtering<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Sometimes you need to filter on the event&#39;s data, not just its type. Parse the body and check specific fields.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Filter by Platform<\/h3>\n\n\n\n<pre><code class=\"language-typescript\">async function processEvent(event: any) {\n  \/\/ Only process iOS events\n  if (event.data.platform !== &#39;ios&#39;) return;\n\n  await handleIOSEvent(event);\n}\n<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">Filter by Campaign<\/h3>\n\n\n\n<pre><code class=\"language-typescript\">const MONITORED_CAMPAIGNS = new Set([\n  &#39;paid-social-q2&#39;,\n  &#39;email-newsletter-may&#39;,\n  &#39;influencer-summer&#39;,\n]);\n\nasync function processEvent(event: any) {\n  if (!MONITORED_CAMPAIGNS.has(event.data.campaign)) return;\n\n  await handleMonitoredCampaign(event);\n}\n<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">Filter by Time Window<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Process only events from the last hour (useful when catching up after a receiver restart):<\/p>\n\n\n\n<pre><code class=\"language-typescript\">async function processEvent(event: any) {\n  const eventAge = Date.now() - new Date(event.timestamp).getTime();\n  const ONE_HOUR = 60 * 60 * 1000;\n\n  if (eventAge &gt; ONE_HOUR) {\n    console.log(`Skipping stale event from ${event.timestamp}`);\n    return;\n  }\n\n  await handleEvent(event);\n}\n<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\">Layer 4: Router Pattern<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">For complex integrations with multiple consumers, build an event router that evaluates rules and dispatches to handlers.<\/p>\n\n\n\n<pre><code class=\"language-typescript\">interface FilterRule {\n  name: string;\n  condition: (event: any) =&gt; boolean;\n  handler: (event: any) =&gt; Promise&lt;void&gt;;\n}\n\nconst rules: FilterRule[] = [\n  {\n    name: &#39;slack-installs&#39;,\n    condition: (e) =&gt; e.event === &#39;install.tracked&#39;,\n    handler: notifySlack,\n  },\n  {\n    name: &#39;crm-referrals&#39;,\n    condition: (e) =&gt;\n      e.event === &#39;referral.completed&#39; &amp;&amp; e.data.platform !== &#39;web&#39;,\n    handler: updateCRM,\n  },\n  {\n    name: &#39;analytics-all&#39;,\n    condition: () =&gt; true,\n    handler: sendToAnalytics,\n  },\n  {\n    name: &#39;fraud-check&#39;,\n    condition: (e) =&gt;\n      e.event === &#39;referral.created&#39; || e.event === &#39;link.clicked&#39;,\n    handler: checkForFraud,\n  },\n];\n\nasync function routeEvent(event: any) {\n  const matching = rules.filter(rule =&gt; rule.condition(event));\n\n  await Promise.allSettled(\n    matching.map(rule =&gt;\n      rule.handler(event).catch(err =&gt;\n        console.error(`Handler ${rule.name} failed:`, err.message)\n      )\n    )\n  );\n}\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Each rule is independent. Adding a new integration means adding a rule, not modifying existing code. <code>Promise.allSettled<\/code> ensures one handler&#39;s failure doesn&#39;t block the others.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Filtering with Queues<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">If you use a message queue between your receiver and processors, filter either before enqueuing (to reduce queue volume) or at the consumer level (to keep the queue general-purpose).<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Filter Before Enqueuing<\/h3>\n\n\n\n<pre><code class=\"language-typescript\">\/\/ Only enqueue high-value events\napp.post(&#39;\/webhooks\/tolinku&#39;, (req, res) =&gt; {\n  \/\/ Verify signature...\n  res.status(200).send(&#39;OK&#39;);\n\n  const eventType = req.headers[&#39;x-webhook-event&#39;] as string;\n  if (eventType === &#39;link.clicked&#39;) return; \/\/ Don&#39;t queue clicks\n\n  queue.add({ body: req.body.toString(), eventType });\n});\n<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">Filter at the Consumer<\/h3>\n\n\n\n<pre><code class=\"language-typescript\">\/\/ Consumer only processes referral events\nworker.process(async (job) =&gt; {\n  if (!job.data.eventType.startsWith(&#39;referral.&#39;)) return;\n\n  const event = JSON.parse(job.data.body);\n  await processReferral(event);\n});\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The first approach reduces queue volume. The second approach keeps the queue general-purpose so you can add new consumers later without changing the receiver.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Topic-Based Routing<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">If your queue supports topics (Kafka, SNS\/SQS), route events to different topics by type:<\/p>\n\n\n\n<pre><code class=\"language-typescript\">const TOPIC_MAP: Record&lt;string, string&gt; = {\n  &#39;link.clicked&#39;: &#39;deep-link-clicks&#39;,\n  &#39;deferred_link.claimed&#39;: &#39;deep-link-installs&#39;,\n  &#39;install.tracked&#39;: &#39;deep-link-installs&#39;,\n  &#39;referral.created&#39;: &#39;deep-link-referrals&#39;,\n  &#39;referral.completed&#39;: &#39;deep-link-referrals&#39;,\n};\n\napp.post(&#39;\/webhooks\/tolinku&#39;, async (req, res) =&gt; {\n  \/\/ Verify signature...\n  res.status(200).send(&#39;OK&#39;);\n\n  const eventType = req.headers[&#39;x-webhook-event&#39;] as string;\n  const topic = TOPIC_MAP[eventType] || &#39;deep-link-other&#39;;\n\n  await kafka.send({\n    topic,\n    messages: [{ value: req.body.toString() }],\n  });\n});\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Each consumer subscribes to the topics it cares about. The clicks consumer subscribes to <code>deep-link-clicks<\/code>; the CRM consumer subscribes to <code>deep-link-referrals<\/code>.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Filtering Best Practices<\/h2>\n\n\n\n<ol class=\"wp-block-list\">\n<li><p><strong>Filter at the source first.<\/strong> Source-level filtering (selecting event types in the Tolinku dashboard) eliminates traffic before it hits your infrastructure. This is the cheapest and most effective filter.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Always respond 200.<\/strong> Even when filtering out an event, respond with 200 to prevent retries. Tolinku interprets non-2xx as a failure and will retry, adding load.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Log filtered events.<\/strong> When you filter out an event, log that you received and discarded it. This helps with debugging (&quot;why isn&#39;t my handler processing link clicks?&quot; &quot;Because your filter excludes them&quot;).<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Review filters periodically.<\/strong> As your integration evolves, you might need events you previously filtered out. A quarterly review of your webhook subscriptions and filter rules prevents stale configurations.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Don&#39;t over-filter.<\/strong> If you&#39;re filtering heavily at the receiver level, you might be better off creating a separate webhook with the right event subscriptions. Multiple focused webhooks are cleaner than one broad webhook with complex filter logic.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">For monitoring what&#39;s being delivered (and filtered), see the <a href=\"https:\/\/tolinku.com\/blog\/webhook-delivery-monitoring\/\">webhook delivery monitoring guide<\/a>. For the full event type reference, see the <a href=\"https:\/\/tolinku.com\/blog\/webhook-event-types\/\">webhook event types guide<\/a>.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Filter webhook events at the source and in your receiver. Subscribe to specific event types, route by content, and reduce noise in your deep link integration.<\/p>\n","protected":false},"author":2,"featured_media":1173,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"rank_math_title":"Webhook Filtering: Receiving Only Events You Need","rank_math_description":"Filter webhook events at the source and in your receiver. Subscribe to specific event types and reduce noise in your deep link integration.","rank_math_focus_keyword":"webhook filtering","rank_math_canonical_url":"","rank_math_facebook_title":"","rank_math_facebook_description":"","rank_math_facebook_image":"https:\/\/tolinku.com\/blog\/wp-content\/uploads\/2026\/03\/og-webhook-filtering.png","rank_math_facebook_image_id":"","rank_math_twitter_title":"","rank_math_twitter_description":"","rank_math_twitter_image":"https:\/\/tolinku.com\/blog\/wp-content\/uploads\/2026\/03\/og-webhook-filtering.png","footnotes":""},"categories":[15],"tags":[254,20,264,300,296,61],"class_list":["post-1174","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-engineering","tag-best-practices","tag-deep-linking","tag-engineering","tag-filtering","tag-performance","tag-webhooks"],"_links":{"self":[{"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/posts\/1174","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/users\/2"}],"replies":[{"embeddable":true,"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/comments?post=1174"}],"version-history":[{"count":2,"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/posts\/1174\/revisions"}],"predecessor-version":[{"id":2267,"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/posts\/1174\/revisions\/2267"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/media\/1173"}],"wp:attachment":[{"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/media?parent=1174"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/categories?post=1174"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/tags?post=1174"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}