Skip to content
Tolinku
Tolinku
Sign In Start Free
Engineering · · 5 min read

Webhook Filtering: Receiving Only Events You Need

By Tolinku Staff
|
Tolinku webhooks integrations dashboard screenshot for engineering blog posts

A webhook endpoint that receives every event type processes a lot of noise. If you only care about install attribution, you don't need thousands of link.clicked events clogging your receiver. If your Slack integration only notifies on referral completions, it shouldn't be parsing click payloads just to discard them.

Filtering reduces load, simplifies handler logic, and keeps your integrations focused. This guide covers filtering strategies for Tolinku webhooks at every layer: source-level event selection, receiver-side routing, and content-based filtering. For the webhook setup, see the webhook setup guide. For payload details, see the webhook event types guide.

Tolinku webhook configuration for event notifications The webhooks page with create form, webhook list, and delivery log.

Layer 1: Filter at the Source

The most efficient filter is the one that prevents events from being sent in the first place. When you configure a webhook in your Tolinku Appspace, you select which event types to subscribe to.

The available event types are:

  • link.clicked
  • deferred_link.claimed
  • install.tracked
  • referral.created
  • referral.completed

If your webhook endpoint only handles referral workflows, subscribe to referral.created and referral.completed. Tolinku won't send the other event types to that endpoint.

Multiple Webhooks for Different Purposes

Instead of one webhook that receives everything and filters in your code, create separate webhooks for each integration:

Webhook Name Events Endpoint
Analytics Pipeline All events https://api.example.com/webhooks/analytics
Slack Notifications install.tracked, referral.completed https://api.example.com/webhooks/slack
CRM Updates referral.created, referral.completed https://api.example.com/webhooks/crm
Email Triggers install.tracked, referral.completed https://api.example.com/webhooks/email

Each endpoint receives only the events it needs. No wasted bandwidth, no wasted processing.

Tolinku supports up to 50 webhooks per Appspace on paid plans (1 on the free plan), so you have room for purpose-specific endpoints.

Layer 2: Filter in the Receiver

Even with source-level filtering, you may want finer-grained control. The X-Webhook-Event header lets you route events before parsing the body.

Header-Based Routing

app.post('/webhooks/tolinku', (req, res) => {
  // Verify signature...
  res.status(200).send('OK');

  const eventType = req.headers['x-webhook-event'] as string;

  switch (eventType) {
    case 'install.tracked':
      handleInstall(req.body);
      break;
    case 'referral.completed':
      handleReferralCompletion(req.body);
      break;
    default:
      // Ignore other event types
      break;
  }
});

This is faster than parsing the JSON body and checking event.event, though the difference is negligible at low volumes.

Allow-List Pattern

Define the events your receiver handles and reject (silently) anything else:

const HANDLED_EVENTS = new Set([
  'install.tracked',
  'referral.created',
  'referral.completed',
]);

app.post('/webhooks/tolinku', (req, res) => {
  // Verify signature...
  res.status(200).send('OK');

  const eventType = req.headers['x-webhook-event'] as string;
  if (!HANDLED_EVENTS.has(eventType)) return;

  const event = JSON.parse(req.body.toString());
  processEvent(event);
});

Always respond 200 even for filtered events. A non-200 response triggers retries, wasting resources on both sides.

Layer 3: Content-Based Filtering

Sometimes you need to filter on the event's data, not just its type. Parse the body and check specific fields.

Filter by Platform

async function processEvent(event: any) {
  // Only process iOS events
  if (event.data.platform !== 'ios') return;

  await handleIOSEvent(event);
}

Filter by Campaign

const MONITORED_CAMPAIGNS = new Set([
  'paid-social-q2',
  'email-newsletter-may',
  'influencer-summer',
]);

async function processEvent(event: any) {
  if (!MONITORED_CAMPAIGNS.has(event.data.campaign)) return;

  await handleMonitoredCampaign(event);
}

Filter by Time Window

Process only events from the last hour (useful when catching up after a receiver restart):

async function processEvent(event: any) {
  const eventAge = Date.now() - new Date(event.timestamp).getTime();
  const ONE_HOUR = 60 * 60 * 1000;

  if (eventAge > ONE_HOUR) {
    console.log(`Skipping stale event from ${event.timestamp}`);
    return;
  }

  await handleEvent(event);
}

Layer 4: Router Pattern

For complex integrations with multiple consumers, build an event router that evaluates rules and dispatches to handlers.

interface FilterRule {
  name: string;
  condition: (event: any) => boolean;
  handler: (event: any) => Promise<void>;
}

const rules: FilterRule[] = [
  {
    name: 'slack-installs',
    condition: (e) => e.event === 'install.tracked',
    handler: notifySlack,
  },
  {
    name: 'crm-referrals',
    condition: (e) =>
      e.event === 'referral.completed' && e.data.platform !== 'web',
    handler: updateCRM,
  },
  {
    name: 'analytics-all',
    condition: () => true,
    handler: sendToAnalytics,
  },
  {
    name: 'fraud-check',
    condition: (e) =>
      e.event === 'referral.created' || e.event === 'link.clicked',
    handler: checkForFraud,
  },
];

async function routeEvent(event: any) {
  const matching = rules.filter(rule => rule.condition(event));

  await Promise.allSettled(
    matching.map(rule =>
      rule.handler(event).catch(err =>
        console.error(`Handler ${rule.name} failed:`, err.message)
      )
    )
  );
}

Each rule is independent. Adding a new integration means adding a rule, not modifying existing code. Promise.allSettled ensures one handler's failure doesn't block the others.

Filtering with Queues

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).

Filter Before Enqueuing

// Only enqueue high-value events
app.post('/webhooks/tolinku', (req, res) => {
  // Verify signature...
  res.status(200).send('OK');

  const eventType = req.headers['x-webhook-event'] as string;
  if (eventType === 'link.clicked') return; // Don't queue clicks

  queue.add({ body: req.body.toString(), eventType });
});

Filter at the Consumer

// Consumer only processes referral events
worker.process(async (job) => {
  if (!job.data.eventType.startsWith('referral.')) return;

  const event = JSON.parse(job.data.body);
  await processReferral(event);
});

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.

Topic-Based Routing

If your queue supports topics (Kafka, SNS/SQS), route events to different topics by type:

const TOPIC_MAP: Record<string, string> = {
  'link.clicked': 'deep-link-clicks',
  'deferred_link.claimed': 'deep-link-installs',
  'install.tracked': 'deep-link-installs',
  'referral.created': 'deep-link-referrals',
  'referral.completed': 'deep-link-referrals',
};

app.post('/webhooks/tolinku', async (req, res) => {
  // Verify signature...
  res.status(200).send('OK');

  const eventType = req.headers['x-webhook-event'] as string;
  const topic = TOPIC_MAP[eventType] || 'deep-link-other';

  await kafka.send({
    topic,
    messages: [{ value: req.body.toString() }],
  });
});

Each consumer subscribes to the topics it cares about. The clicks consumer subscribes to deep-link-clicks; the CRM consumer subscribes to deep-link-referrals.

Filtering Best Practices

  1. Filter at the source first. 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.

    Always respond 200. Even when filtering out an event, respond with 200 to prevent retries. Tolinku interprets non-2xx as a failure and will retry, adding load.

    Log filtered events. When you filter out an event, log that you received and discarded it. This helps with debugging ("why isn't my handler processing link clicks?" "Because your filter excludes them").

    Review filters periodically. 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.

    Don't over-filter. If you'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.

    For monitoring what's being delivered (and filtered), see the webhook delivery monitoring guide. For the full event type reference, see the webhook event types guide.

Get deep linking tips in your inbox

One email per week. No spam.

Ready to add deep linking to your app?

Set up Universal Links, App Links, deferred deep linking, and analytics in minutes. Free to start.