{"id":1153,"date":"2026-05-22T09:00:00","date_gmt":"2026-05-22T14:00:00","guid":{"rendered":"https:\/\/tolinku.com\/blog\/?p=1153"},"modified":"2026-03-07T03:34:58","modified_gmt":"2026-03-07T08:34:58","slug":"webhook-slack-notifications","status":"publish","type":"post","link":"https:\/\/tolinku.com\/blog\/webhook-slack-notifications\/","title":{"rendered":"Slack Notifications for Deep Link Events via Webhooks"},"content":{"rendered":"\n<p class=\"wp-block-paragraph\">There&#39;s something motivating about watching deep link events roll into Slack in real time. The growth team sees an install notification and knows a campaign is working. The engineering team sees a webhook failure and investigates before it becomes a support ticket. A product manager sees a referral completion and knows the referral flow is functioning.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">This guide covers three ways to send <a href=\"https:\/\/tolinku.com\/features\/webhooks\">Tolinku webhook<\/a> events to Slack: a direct integration using Slack&#39;s Incoming Webhooks, a custom receiver with rich message formatting, and a no-code path through Zapier. Pick the one that matches your team&#39;s engineering bandwidth.<\/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\">Option 1: Direct Slack Incoming Webhook<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">The simplest approach. Slack&#39;s <a href=\"https:\/\/api.slack.com\/messaging\/webhooks\" rel=\"nofollow noopener\" target=\"_blank\">Incoming Webhooks<\/a> let you POST a JSON message to a channel URL. No Slack app installation needed (just a webhook URL from Slack).<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Set Up the Slack Webhook<\/h3>\n\n\n\n<ol class=\"wp-block-list\">\n<li>Go to <a href=\"https:\/\/api.slack.com\/apps\" rel=\"nofollow noopener\" target=\"_blank\">api.slack.com\/apps<\/a> and create a new app (or use an existing one)<\/li>\n<li>Under <strong>Features &gt; Incoming Webhooks<\/strong>, toggle it on<\/li>\n<li>Click <strong>Add New Webhook to Workspace<\/strong> and select the channel (e.g., #deep-link-events)<\/li>\n<li>Copy the webhook URL (e.g., <code>https:\/\/hooks.slack.com\/services\/T00000000\/B00000000\/XXXXXXXXXXXXXXXXXXXXXXXX<\/code>)<\/li>\n<\/ol>\n\n\n\n<h3 class=\"wp-block-heading\">Build the Receiver<\/h3>\n\n\n\n<pre><code class=\"language-typescript\">import express from &#39;express&#39;;\nimport crypto from &#39;crypto&#39;;\n\nconst app = express();\napp.use(&#39;\/webhooks&#39;, express.raw({ type: &#39;application\/json&#39; }));\n\nconst WEBHOOK_SECRET = process.env.WEBHOOK_SECRET!;\nconst SLACK_WEBHOOK_URL = process.env.SLACK_WEBHOOK_URL!;\n\napp.post(&#39;\/webhooks\/tolinku&#39;, async (req, res) =&gt; {\n  const signature = req.headers[&#39;x-webhook-signature&#39;] as string;\n  const expected = crypto\n    .createHmac(&#39;sha256&#39;, WEBHOOK_SECRET)\n    .update(req.body)\n    .digest(&#39;hex&#39;);\n\n  if (signature !== expected) {\n    return res.status(401).send(&#39;Invalid signature&#39;);\n  }\n\n  res.status(200).send(&#39;OK&#39;);\n\n  const event = JSON.parse(req.body.toString());\n  await sendToSlack(event);\n});\n\nasync function sendToSlack(event: any) {\n  const message = formatSlackMessage(event);\n\n  await fetch(SLACK_WEBHOOK_URL, {\n    method: &#39;POST&#39;,\n    headers: { &#39;Content-Type&#39;: &#39;application\/json&#39; },\n    body: JSON.stringify(message),\n  });\n}\n\napp.listen(3000);\n<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">Format Messages<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Plain text notifications work, but <a href=\"https:\/\/api.slack.com\/reference\/block-kit\" rel=\"nofollow noopener\" target=\"_blank\">Block Kit<\/a> messages are easier to scan in a busy channel.<\/p>\n\n\n\n<pre><code class=\"language-typescript\">function formatSlackMessage(event: any) {\n  const { event: eventType, timestamp, data } = event;\n  const time = new Date(timestamp).toLocaleString(&#39;en-US&#39;, {\n    timeZone: &#39;UTC&#39;,\n    hour12: false,\n  });\n\n  const emoji = getEmoji(eventType);\n  const title = getTitle(eventType);\n\n  const fields: Array&lt;{ type: string; text: string }&gt; = [];\n\n  if (data.campaign) {\n    fields.push({\n      type: &#39;mrkdwn&#39;,\n      text: `*Campaign:* ${data.campaign}`,\n    });\n  }\n  if (data.platform) {\n    fields.push({\n      type: &#39;mrkdwn&#39;,\n      text: `*Platform:* ${data.platform}`,\n    });\n  }\n  if (data.token) {\n    fields.push({\n      type: &#39;mrkdwn&#39;,\n      text: `*Link:* ${data.prefix || &#39;&#39;}\/${data.token}`,\n    });\n  }\n  if (data.device_type) {\n    fields.push({\n      type: &#39;mrkdwn&#39;,\n      text: `*Device:* ${data.device_type}`,\n    });\n  }\n\n  return {\n    blocks: [\n      {\n        type: &#39;section&#39;,\n        text: {\n          type: &#39;mrkdwn&#39;,\n          text: `${emoji} *${title}*`,\n        },\n      },\n      ...(fields.length &gt; 0\n        ? [\n            {\n              type: &#39;section&#39;,\n              fields,\n            },\n          ]\n        : []),\n      {\n        type: &#39;context&#39;,\n        elements: [\n          {\n            type: &#39;mrkdwn&#39;,\n            text: `${time} UTC`,\n          },\n        ],\n      },\n    ],\n  };\n}\n\nfunction getEmoji(eventType: string): string {\n  const emojis: Record&lt;string, string&gt; = {\n    &#39;link.clicked&#39;: &#39;:link:&#39;,\n    &#39;deferred_link.claimed&#39;: &#39;:calling:&#39;,\n    &#39;install.tracked&#39;: &#39;:iphone:&#39;,\n    &#39;referral.created&#39;: &#39;:handshake:&#39;,\n    &#39;referral.completed&#39;: &#39;:tada:&#39;,\n    &#39;test&#39;: &#39;:test_tube:&#39;,\n  };\n  return emojis[eventType] || &#39;:bell:&#39;;\n}\n\nfunction getTitle(eventType: string): string {\n  const titles: Record&lt;string, string&gt; = {\n    &#39;link.clicked&#39;: &#39;Link Clicked&#39;,\n    &#39;deferred_link.claimed&#39;: &#39;Deferred Link Claimed&#39;,\n    &#39;install.tracked&#39;: &#39;Install Tracked&#39;,\n    &#39;referral.created&#39;: &#39;Referral Created&#39;,\n    &#39;referral.completed&#39;: &#39;Referral Completed&#39;,\n    &#39;test&#39;: &#39;Test Webhook&#39;,\n  };\n  return titles[eventType] || eventType;\n}\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">This produces a clean, structured Slack message with the event type, relevant fields, and timestamp.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Option 2: Filtered Notifications<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">You probably don&#39;t want every <code>link.clicked<\/code> event flooding your Slack channel. High-traffic links can generate thousands of clicks per day. Here are strategies for keeping the signal-to-noise ratio high.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Filter at the Source<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">When configuring your webhook in <a href=\"https:\/\/tolinku.com\/docs\/user-guide\/webhooks\/\">Tolinku<\/a>, select only the events you want notifications for. For most teams, <code>install.tracked<\/code>, <code>referral.created<\/code>, and <code>referral.completed<\/code> are the high-value events worth notifying on. Leave <code>link.clicked<\/code> for your analytics pipeline instead.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Filter in the Receiver<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">If you subscribe to multiple events on one webhook, filter in your handler:<\/p>\n\n\n\n<pre><code class=\"language-typescript\">const NOTIFY_EVENTS = new Set([\n  &#39;install.tracked&#39;,\n  &#39;referral.created&#39;,\n  &#39;referral.completed&#39;,\n]);\n\nasync function sendToSlack(event: any) {\n  if (!NOTIFY_EVENTS.has(event.event)) return;\n\n  const message = formatSlackMessage(event);\n  await fetch(SLACK_WEBHOOK_URL, {\n    method: &#39;POST&#39;,\n    headers: { &#39;Content-Type&#39;: &#39;application\/json&#39; },\n    body: JSON.stringify(message),\n  });\n}\n<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">Aggregate Instead of Notify Per Event<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">For click events, a periodic summary is more useful than individual notifications. Buffer events and send a digest:<\/p>\n\n\n\n<pre><code class=\"language-typescript\">let clickBuffer: any[] = [];\n\nsetInterval(async () =&gt; {\n  if (clickBuffer.length === 0) return;\n\n  const count = clickBuffer.length;\n  const campaigns = new Map&lt;string, number&gt;();\n  for (const event of clickBuffer) {\n    const campaign = event.data.campaign || &#39;unknown&#39;;\n    campaigns.set(campaign, (campaigns.get(campaign) || 0) + 1);\n  }\n\n  const campaignList = [...campaigns.entries()]\n    .sort((a, b) =&gt; b[1] - a[1])\n    .map(([name, n]) =&gt; `  ${name}: ${n}`)\n    .join(&#39;\\n&#39;);\n\n  await fetch(SLACK_WEBHOOK_URL, {\n    method: &#39;POST&#39;,\n    headers: { &#39;Content-Type&#39;: &#39;application\/json&#39; },\n    body: JSON.stringify({\n      text: `:chart_with_upwards_trend: *${count} link clicks* in the last 15 minutes\\n${campaignList}`,\n    }),\n  });\n\n  clickBuffer = [];\n}, 15 * 60 * 1000); \/\/ Every 15 minutes\n<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\">Option 3: Multiple Channels<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Route different events to different Slack channels for the right audience:<\/p>\n\n\n\n<pre><code class=\"language-typescript\">const CHANNEL_MAP: Record&lt;string, string&gt; = {\n  &#39;install.tracked&#39;: process.env.SLACK_GROWTH_CHANNEL!,\n  &#39;referral.created&#39;: process.env.SLACK_GROWTH_CHANNEL!,\n  &#39;referral.completed&#39;: process.env.SLACK_SALES_CHANNEL!,\n  &#39;link.clicked&#39;: process.env.SLACK_ANALYTICS_CHANNEL!,\n};\n\nasync function sendToSlack(event: any) {\n  const channelUrl = CHANNEL_MAP[event.event];\n  if (!channelUrl) return;\n\n  const message = formatSlackMessage(event);\n  await fetch(channelUrl, {\n    method: &#39;POST&#39;,\n    headers: { &#39;Content-Type&#39;: &#39;application\/json&#39; },\n    body: JSON.stringify(message),\n  });\n}\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Each Slack Incoming Webhook is bound to a specific channel, so you need a separate webhook URL for each destination channel.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Option 4: No-Code with Zapier<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">If you don&#39;t want to host a receiver, use <a href=\"https:\/\/tolinku.com\/blog\/webhook-zapier-integration\/\">Zapier<\/a> to connect Tolinku webhooks to Slack:<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li>Create a Zap with &quot;Webhooks by Zapier&quot; as the trigger (Catch Hook)<\/li>\n<li>Add a Filter step to select the event types you want<\/li>\n<li>Add a Slack action: &quot;Send Channel Message&quot;<\/li>\n<li>Map the webhook fields into the message<\/li>\n<\/ol>\n\n\n\n<p class=\"wp-block-paragraph\">The tradeoff: Zapier doesn&#39;t verify webhook signatures, and delivery depends on your Zapier plan&#39;s polling interval. For non-critical notifications, this is fine. For production alerting, use a custom receiver.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Handling Failures<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">If Slack&#39;s API is down or rate-limited, your notification will fail. This shouldn&#39;t affect your webhook processing. Always:<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li>Respond 200 to Tolinku before attempting the Slack notification<\/li>\n<li>Catch and log Slack API errors without rethrowing<\/li>\n<li>Consider a simple retry with exponential backoff for Slack failures<\/li>\n<\/ol>\n\n\n\n<pre><code class=\"language-typescript\">async function sendToSlack(event: any) {\n  if (!NOTIFY_EVENTS.has(event.event)) return;\n\n  const message = formatSlackMessage(event);\n\n  for (let attempt = 0; attempt &lt; 3; attempt++) {\n    try {\n      const resp = await fetch(SLACK_WEBHOOK_URL, {\n        method: &#39;POST&#39;,\n        headers: { &#39;Content-Type&#39;: &#39;application\/json&#39; },\n        body: JSON.stringify(message),\n      });\n      if (resp.ok) return;\n      if (resp.status === 429) {\n        \/\/ Rate limited; wait and retry\n        const retryAfter = parseInt(resp.headers.get(&#39;retry-after&#39;) || &#39;5&#39;);\n        await new Promise(r =&gt; setTimeout(r, retryAfter * 1000));\n        continue;\n      }\n      console.error(`Slack API error: ${resp.status}`);\n      return;\n    } catch (err) {\n      console.error(&#39;Slack send failed:&#39;, err);\n      if (attempt &lt; 2) await new Promise(r =&gt; setTimeout(r, 1000 * (attempt + 1)));\n    }\n  }\n}\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Slack&#39;s webhook API has a rate limit of <a href=\"https:\/\/api.slack.com\/docs\/rate-limits\" rel=\"nofollow noopener\" target=\"_blank\">1 message per second per webhook URL<\/a>. If you&#39;re sending more than that, batch or aggregate your notifications.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">For setup instructions, see the <a href=\"https:\/\/tolinku.com\/blog\/webhook-setup-guide\/\">webhook setup guide<\/a>. To verify your integration, use the tools in the <a href=\"https:\/\/tolinku.com\/blog\/webhook-testing-tools\/\">webhook testing guide<\/a>.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Get real-time Slack notifications when deep links are clicked, apps are installed, or referrals complete. Set up webhook-to-Slack integrations in minutes.<\/p>\n","protected":false},"author":2,"featured_media":1152,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"rank_math_title":"Slack Notifications for Deep Link Events via Webhooks","rank_math_description":"Get real-time Slack notifications when deep links are clicked, apps are installed, or referrals complete. Set up webhook-to-Slack integrations in minutes.","rank_math_focus_keyword":"webhook Slack notification","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-slack-notifications.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-slack-notifications.png","footnotes":""},"categories":[15],"tags":[165,20,264,263,289,288,61],"class_list":["post-1153","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-engineering","tag-automation","tag-deep-linking","tag-engineering","tag-integrations","tag-notifications","tag-slack","tag-webhooks"],"_links":{"self":[{"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/posts\/1153","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=1153"}],"version-history":[{"count":2,"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/posts\/1153\/revisions"}],"predecessor-version":[{"id":2260,"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/posts\/1153\/revisions\/2260"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/media\/1152"}],"wp:attachment":[{"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/media?parent=1153"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/categories?post=1153"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/tags?post=1153"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}