{"id":1147,"date":"2026-05-21T13:00:00","date_gmt":"2026-05-21T18:00:00","guid":{"rendered":"https:\/\/tolinku.com\/blog\/?p=1147"},"modified":"2026-03-07T03:34:57","modified_gmt":"2026-03-07T08:34:57","slug":"webhook-attribution","status":"publish","type":"post","link":"https:\/\/tolinku.com\/blog\/webhook-attribution\/","title":{"rendered":"Attribution via Webhooks: Real-Time Conversion Tracking"},"content":{"rendered":"\n<p class=\"wp-block-paragraph\">Attribution answers the question &quot;where did this user come from?&quot; Webhooks answer the question &quot;when?&quot; Together, they give you real-time conversion tracking: the moment a user installs your app, completes a referral, or claims a deferred deep link, you know about it and you know which campaign, channel, and link brought them in.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">This guide covers how to use <a href=\"https:\/\/tolinku.com\/features\/webhooks\">Tolinku webhooks<\/a> for real-time attribution, how to build an attribution data model, and how to connect webhook events to campaign optimization workflows. For the general webhook setup, see the <a href=\"https:\/\/tolinku.com\/blog\/webhook-setup-guide\/\">webhook setup guide<\/a>. For a broader overview of mobile attribution, see the <a href=\"https:\/\/tolinku.com\/blog\/mobile-attribution-developers-guide\/\">mobile attribution developer&#39;s 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\">The Attribution Events<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Tolinku fires five webhook events. Each one carries attribution context in the <code>data<\/code> object:<\/p>\n\n\n\n<h3 class=\"wp-block-heading\"><code>link.clicked<\/code><\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Fires when a user taps or clicks a deep link. This is the top of your attribution funnel.<\/p>\n\n\n\n<pre><code class=\"language-json\">{\n  &quot;event&quot;: &quot;link.clicked&quot;,\n  &quot;timestamp&quot;: &quot;2026-05-21T14:00:00.000Z&quot;,\n  &quot;data&quot;: {\n    &quot;prefix&quot;: &quot;go&quot;,\n    &quot;token&quot;: &quot;summer-sale&quot;,\n    &quot;hostname&quot;: &quot;links.example.com&quot;,\n    &quot;ip&quot;: &quot;203.0.113.42&quot;,\n    &quot;platform&quot;: &quot;ios&quot;,\n    &quot;device_type&quot;: &quot;mobile&quot;,\n    &quot;campaign&quot;: &quot;instagram-story-may&quot;\n  }\n}\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The <code>campaign<\/code> field corresponds to the campaign parameter configured on the link. The <code>prefix<\/code> and <code>token<\/code> together identify the specific route that was clicked (e.g., <code>go\/summer-sale<\/code>).<\/p>\n\n\n\n<h3 class=\"wp-block-heading\"><code>deferred_link.claimed<\/code><\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Fires when a deferred deep link is resolved after app install. This event proves that a pre-install click led to an actual install and app open.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\"><code>install.tracked<\/code><\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Fires when an app install is attributed to a deep link. This is the core conversion event for user acquisition campaigns.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\"><code>referral.created<\/code><\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Fires when a new referral is registered. The referred user clicked a referral link and started the signup process.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\"><code>referral.completed<\/code><\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Fires when a referred user completes the target action (first purchase, subscription, etc.). This is the bottom of the referral attribution funnel.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">See the <a href=\"https:\/\/tolinku.com\/blog\/webhook-event-types\/\">webhook event types guide<\/a> for full payload documentation.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Building an Attribution Model<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">For real-time attribution, you need to connect events across the funnel. A single user journey might produce this sequence:<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li><code>link.clicked<\/code> (user taps a campaign link)<\/li>\n<li><code>deferred_link.claimed<\/code> (user installs and opens the app; the original click is resolved)<\/li>\n<li><code>install.tracked<\/code> (install is attributed)<\/li>\n<li><code>referral.completed<\/code> (if this was a referral link, the conversion fires)<\/li>\n<\/ol>\n\n\n\n<p class=\"wp-block-paragraph\">The link between these events is the deep link itself: the <code>prefix<\/code>, <code>token<\/code>, and <code>campaign<\/code> fields appear consistently across related events. Your attribution system matches events by these fields to reconstruct the user journey.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Attribution Table Schema<\/h3>\n\n\n\n<pre><code class=\"language-sql\">CREATE TABLE attribution_events (\n  id SERIAL PRIMARY KEY,\n  event_type VARCHAR(50) NOT NULL,\n  event_timestamp TIMESTAMPTZ NOT NULL,\n  received_at TIMESTAMPTZ DEFAULT NOW(),\n  prefix VARCHAR(100),\n  token VARCHAR(255),\n  hostname VARCHAR(255),\n  campaign VARCHAR(255),\n  platform VARCHAR(20),\n  device_type VARCHAR(20),\n  ip INET,\n  raw_data JSONB NOT NULL\n);\n\nCREATE INDEX idx_attribution_campaign ON attribution_events(campaign);\nCREATE INDEX idx_attribution_token ON attribution_events(token);\nCREATE INDEX idx_attribution_event_type ON attribution_events(event_type);\nCREATE INDEX idx_attribution_timestamp ON attribution_events(event_timestamp);\n<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">Receiver<\/h3>\n\n\n\n<pre><code class=\"language-typescript\">import express from &#39;express&#39;;\nimport crypto from &#39;crypto&#39;;\nimport pg from &#39;pg&#39;;\n\nconst app = express();\napp.use(&#39;\/webhooks&#39;, express.raw({ type: &#39;application\/json&#39; }));\n\nconst pool = new pg.Pool({ connectionString: process.env.DATABASE_URL });\nconst WEBHOOK_SECRET = process.env.WEBHOOK_SECRET!;\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 recordAttribution(event);\n});\n\nasync function recordAttribution(event: any) {\n  const { event: eventType, timestamp, data } = event;\n\n  \/\/ Deduplicate using a hash of the event content\n  const eventHash = crypto\n    .createHash(&#39;sha256&#39;)\n    .update(JSON.stringify(event))\n    .digest(&#39;hex&#39;);\n\n  await pool.query(\n    `INSERT INTO attribution_events\n     (event_type, event_timestamp, prefix, token, hostname, campaign,\n      platform, device_type, ip, raw_data)\n     VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)\n     ON CONFLICT DO NOTHING`,\n    [\n      eventType,\n      timestamp,\n      data.prefix || null,\n      data.token || null,\n      data.hostname || null,\n      data.campaign || null,\n      data.platform || null,\n      data.device_type || null,\n      data.ip || null,\n      JSON.stringify(event),\n    ]\n  );\n}\n<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\">Last-Click Attribution<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">The simplest attribution model: credit the conversion to the last link clicked before the conversion event. This is what most teams start with, and it&#39;s often sufficient.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">With webhook data, last-click attribution is straightforward:<\/p>\n\n\n\n<pre><code class=\"language-sql\">-- For each install, find the most recent click with the same token\nSELECT\n  i.event_timestamp AS install_time,\n  i.token,\n  i.campaign,\n  i.platform,\n  c.event_timestamp AS click_time,\n  EXTRACT(EPOCH FROM (i.event_timestamp - c.event_timestamp)) AS seconds_to_install\nFROM attribution_events i\nLEFT JOIN LATERAL (\n  SELECT event_timestamp\n  FROM attribution_events\n  WHERE event_type = &#39;link.clicked&#39;\n    AND token = i.token\n    AND event_timestamp &lt; i.event_timestamp\n  ORDER BY event_timestamp DESC\n  LIMIT 1\n) c ON true\nWHERE i.event_type = &#39;install.tracked&#39;\n  AND i.event_timestamp &gt; NOW() - INTERVAL &#39;7 days&#39;;\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">This query joins each install event with its most recent preceding click event using the <code>token<\/code> field. The <code>seconds_to_install<\/code> column tells you how long the conversion took.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Multi-Touch Attribution<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">For campaigns that span multiple touchpoints (a user sees an ad, clicks a social post, then clicks a referral link), you want to track all touches, not just the last one.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The webhook data alone won&#39;t give you the full multi-touch picture, because Tolinku fires webhooks for its own link events. But you can combine webhook data with your other attribution sources to build a multi-touch model:<\/p>\n\n\n\n<pre><code class=\"language-sql\">-- All touchpoints for links with the same campaign, ordered by time\nSELECT\n  event_type,\n  event_timestamp,\n  token,\n  campaign,\n  platform,\n  device_type\nFROM attribution_events\nWHERE campaign = &#39;instagram-story-may&#39;\nORDER BY event_timestamp;\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">This shows the full journey of a campaign: clicks, deferred link claims, installs, and referral completions over time.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Real-Time Campaign Dashboards<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">The real power of webhook-based attribution is speed. Instead of waiting for a daily batch export, you can build dashboards that update in real time.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Conversion Rate by Campaign<\/h3>\n\n\n\n<pre><code class=\"language-sql\">SELECT\n  campaign,\n  COUNT(*) FILTER (WHERE event_type = &#39;link.clicked&#39;) AS clicks,\n  COUNT(*) FILTER (WHERE event_type = &#39;install.tracked&#39;) AS installs,\n  COUNT(*) FILTER (WHERE event_type = &#39;referral.completed&#39;) AS conversions,\n  ROUND(\n    COUNT(*) FILTER (WHERE event_type = &#39;install.tracked&#39;)::numeric \/\n    NULLIF(COUNT(*) FILTER (WHERE event_type = &#39;link.clicked&#39;), 0) * 100,\n    2\n  ) AS install_rate,\n  ROUND(\n    COUNT(*) FILTER (WHERE event_type = &#39;referral.completed&#39;)::numeric \/\n    NULLIF(COUNT(*) FILTER (WHERE event_type = &#39;install.tracked&#39;), 0) * 100,\n    2\n  ) AS conversion_rate\nFROM attribution_events\nWHERE event_timestamp &gt; NOW() - INTERVAL &#39;24 hours&#39;\n  AND campaign IS NOT NULL\nGROUP BY campaign\nORDER BY clicks DESC;\n<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">Platform Breakdown<\/h3>\n\n\n\n<pre><code class=\"language-sql\">SELECT\n  platform,\n  device_type,\n  COUNT(*) FILTER (WHERE event_type = &#39;link.clicked&#39;) AS clicks,\n  COUNT(*) FILTER (WHERE event_type = &#39;install.tracked&#39;) AS installs\nFROM attribution_events\nWHERE event_timestamp &gt; NOW() - INTERVAL &#39;7 days&#39;\nGROUP BY platform, device_type\nORDER BY clicks DESC;\n<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">Time to Install<\/h3>\n\n\n\n<pre><code class=\"language-sql\">SELECT\n  campaign,\n  PERCENTILE_CONT(0.5) WITHIN GROUP (\n    ORDER BY EXTRACT(EPOCH FROM (i.event_timestamp - c.event_timestamp))\n  ) AS median_seconds_to_install\nFROM attribution_events i\nJOIN attribution_events c\n  ON c.event_type = &#39;link.clicked&#39;\n  AND c.token = i.token\n  AND c.event_timestamp &lt; i.event_timestamp\nWHERE i.event_type = &#39;install.tracked&#39;\n  AND i.event_timestamp &gt; NOW() - INTERVAL &#39;7 days&#39;\nGROUP BY i.campaign;\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Connect these queries to a dashboard tool (Grafana, Metabase, or a custom frontend) and you have real-time attribution reporting.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Attribution Windows<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">An attribution window defines how long after a click you&#39;ll still credit a conversion. Without a window, a user who clicked a link six months ago and happens to install your app today would get attributed to that old campaign.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Standard attribution windows:<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table>\n<thead>\n<tr>\n<th>Event Flow<\/th>\n<th>Typical Window<\/th>\n<\/tr>\n<\/thead>\n<tbody><tr>\n<td>Click to install<\/td>\n<td>7 days<\/td>\n<\/tr>\n<tr>\n<td>Click to deferred link claim<\/td>\n<td>7 days<\/td>\n<\/tr>\n<tr>\n<td>Install to referral completion<\/td>\n<td>30 days<\/td>\n<\/tr>\n<\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">Apply the window in your queries:<\/p>\n\n\n\n<pre><code class=\"language-sql\">-- Only attribute installs that happened within 7 days of a click\nSELECT *\nFROM attribution_events i\nJOIN attribution_events c\n  ON c.event_type = &#39;link.clicked&#39;\n  AND c.token = i.token\n  AND c.event_timestamp &lt; i.event_timestamp\n  AND i.event_timestamp - c.event_timestamp &lt; INTERVAL &#39;7 days&#39;\nWHERE i.event_type = &#39;install.tracked&#39;;\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Tolinku&#39;s <a href=\"https:\/\/tolinku.com\/features\/analytics\">analytics dashboard<\/a> applies similar windows to its built-in attribution reporting.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Alerting on Attribution Anomalies<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Real-time data enables real-time alerts. Set up monitoring for:<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Click spike without installs<\/strong>: If <code>link.clicked<\/code> volume surges but <code>install.tracked<\/code> stays flat, something is wrong. Either the landing page is broken, the app store listing isn&#39;t converting, or you&#39;re getting bot traffic.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Sudden conversion drop<\/strong>: If a campaign&#39;s conversion rate drops by more than 50% compared to its trailing 7-day average, alert immediately. This could indicate a broken deep link, a misconfigured route, or a tracking issue.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Unusual referral patterns<\/strong>: A single referral token generating hundreds of <code>referral.created<\/code> events in an hour could indicate fraud. See the <a href=\"https:\/\/tolinku.com\/blog\/webhook-fraud-detection\/\">webhook fraud detection<\/a> article for more on this.<\/p>\n\n\n\n<pre><code class=\"language-typescript\">\/\/ Simple anomaly detection: alert if install rate drops below threshold\nasync function checkConversionHealth(campaign: string) {\n  const result = await pool.query(`\n    SELECT\n      COUNT(*) FILTER (WHERE event_type = &#39;link.clicked&#39;) AS clicks,\n      COUNT(*) FILTER (WHERE event_type = &#39;install.tracked&#39;) AS installs\n    FROM attribution_events\n    WHERE campaign = $1\n      AND event_timestamp &gt; NOW() - INTERVAL &#39;1 hour&#39;\n  `, [campaign]);\n\n  const { clicks, installs } = result.rows[0];\n  const rate = clicks &gt; 0 ? installs \/ clicks : 0;\n\n  if (clicks &gt; 100 &amp;&amp; rate &lt; 0.01) {\n    \/\/ Less than 1% install rate on 100+ clicks: something is wrong\n    await sendAlert(`Low install rate for campaign ${campaign}: ${(rate * 100).toFixed(1)}%`);\n  }\n}\n<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\">Privacy Considerations<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Attribution data includes IP addresses and device information. Handle it responsibly:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>IP addresses<\/strong>: Use for geo-enrichment (country\/region), then hash or discard the raw IP. Don&#39;t store raw IPs longer than necessary for attribution matching.<\/li>\n<li><strong>GDPR\/CCPA<\/strong>: If users opt out of tracking, respect that. Tolinku&#39;s webhook events fire based on link clicks, not user consent. Your receiver is responsible for checking consent status before storing attribution data.<\/li>\n<li><strong>Data retention<\/strong>: Set a retention policy. Attribution data older than 90 days is rarely useful for campaign optimization. Aggregate the insights, then purge the raw events.<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">See the <a href=\"https:\/\/tolinku.com\/blog\/webhook-compliance\/\">webhook compliance guide<\/a> for a deeper look at data handling requirements.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Connecting to Other Systems<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Attribution data becomes more powerful when it flows to your existing tools:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>CRM<\/strong>: Update lead records with campaign and conversion data. See the <a href=\"https:\/\/tolinku.com\/blog\/webhook-crm-integration\/\">CRM integration guide<\/a>.<\/li>\n<li><strong>Analytics<\/strong>: Forward events to Amplitude, Mixpanel, or Segment for product analytics. See the <a href=\"https:\/\/tolinku.com\/blog\/webhooks-analytics-pipelines\/\">analytics pipelines guide<\/a>.<\/li>\n<li><strong>Ad platforms<\/strong>: Use conversion data to optimize campaigns in Google Ads, Meta Ads, or TikTok Ads via their server-side conversion APIs.<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">Start by configuring webhooks in your <a href=\"https:\/\/tolinku.com\/docs\/user-guide\/webhooks\/\">Tolinku Appspace<\/a>, deploy a receiver to store events, and build the attribution queries that matter to your team.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Track conversions in real time via webhooks. Receive attribution data for installs, referrals, and revenue as they happen for immediate action.<\/p>\n","protected":false},"author":2,"featured_media":1146,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"rank_math_title":"Attribution via Webhooks: Real-Time Conversion Tracking","rank_math_description":"Track conversions in real time via webhooks. Receive attribution data for installs, referrals, and revenue as they happen for immediate action.","rank_math_focus_keyword":"webhook attribution","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-attribution.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-attribution.png","footnotes":""},"categories":[15],"tags":[37,28,191,20,264,279,61],"class_list":["post-1147","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-engineering","tag-analytics","tag-attribution","tag-conversions","tag-deep-linking","tag-engineering","tag-real-time-data","tag-webhooks"],"_links":{"self":[{"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/posts\/1147","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=1147"}],"version-history":[{"count":2,"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/posts\/1147\/revisions"}],"predecessor-version":[{"id":2258,"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/posts\/1147\/revisions\/2258"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/media\/1146"}],"wp:attachment":[{"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/media?parent=1147"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/categories?post=1147"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/tags?post=1147"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}