{"id":1755,"date":"2026-07-15T13:00:00","date_gmt":"2026-07-15T18:00:00","guid":{"rendered":"https:\/\/tolinku.com\/blog\/?p=1755"},"modified":"2026-03-07T03:50:07","modified_gmt":"2026-03-07T08:50:07","slug":"server-to-server-attribution","status":"publish","type":"post","link":"https:\/\/tolinku.com\/blog\/server-to-server-attribution\/","title":{"rendered":"Server-to-Server Attribution: Architecture Guide"},"content":{"rendered":"\n<p class=\"wp-block-paragraph\">Server-to-server (S2S) attribution moves attribution logic from the client (app SDK) to the server. Instead of the attribution SDK on the device making decisions, your server receives events from the app, queries ad networks&#39; APIs, performs the attribution match, and sends postbacks. This approach gives you more control, better privacy, and independence from third-party SDKs.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">This guide covers S2S attribution architecture. For server-side deferred matching, see <a href=\"https:\/\/tolinku.com\/blog\/server-side-deferred-matching\/\">server-side deferred matching: architecture guide<\/a>. For postback URLs, see <a href=\"https:\/\/tolinku.com\/blog\/postback-urls-attribution\/\">postback URLs for mobile attribution<\/a>.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Why Server-to-Server<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\">Benefits<\/h3>\n\n\n\n<figure class=\"wp-block-table\"><table>\n<thead>\n<tr>\n<th>Benefit<\/th>\n<th>Explanation<\/th>\n<\/tr>\n<\/thead>\n<tbody><tr>\n<td>Privacy control<\/td>\n<td>User data stays on your server. No third-party SDK phoning home.<\/td>\n<\/tr>\n<tr>\n<td>SDK independence<\/td>\n<td>No attribution SDK in your app. Reduces app size, avoids SDK conflicts.<\/td>\n<\/tr>\n<tr>\n<td>Data ownership<\/td>\n<td>You own the raw attribution data, not a third-party provider.<\/td>\n<\/tr>\n<tr>\n<td>Flexibility<\/td>\n<td>Custom attribution logic, custom windows, custom fraud rules.<\/td>\n<\/tr>\n<tr>\n<td>Security<\/td>\n<td>Server-to-server communication uses API keys, not client-side tokens.<\/td>\n<\/tr>\n<\/tbody><\/table><\/figure>\n\n\n\n<h3 class=\"wp-block-heading\">Trade-Offs<\/h3>\n\n\n\n<figure class=\"wp-block-table\"><table>\n<thead>\n<tr>\n<th>Trade-Off<\/th>\n<th>Explanation<\/th>\n<\/tr>\n<\/thead>\n<tbody><tr>\n<td>Engineering cost<\/td>\n<td>You build and maintain the attribution system.<\/td>\n<\/tr>\n<tr>\n<td>SAN integration<\/td>\n<td>Self-attributing networks (Meta, Google) have complex S2S APIs.<\/td>\n<\/tr>\n<tr>\n<td>Maintenance<\/td>\n<td>SDK updates for platform changes (ATT, Privacy Sandbox) are your responsibility.<\/td>\n<\/tr>\n<tr>\n<td>No managed dashboard<\/td>\n<td>You build your own reporting (or use a BI tool).<\/td>\n<\/tr>\n<\/tbody><\/table><\/figure>\n\n\n\n<h2 class=\"wp-block-heading\">Architecture<\/h2>\n\n\n\n<pre><code>\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510     \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510     \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n\u2502  Mobile App \u2502\u2500\u2500\u2500\u2500\u2192\u2502  Your Server \u2502\u2500\u2500\u2500\u2500\u2192\u2502  Ad Networks  \u2502\n\u2502  (minimal)  \u2502     \u2502  (attribution\u2502     \u2502  (postbacks)  \u2502\n\u2502             \u2502     \u2502   logic)     \u2502     \u2502              \u2502\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518     \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518     \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n                           \u2502\n                           \u2193\n                    \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n                    \u2502  Click Store \u2502\n                    \u2502  (Redis\/DB)  \u2502\n                    \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">Components<\/h3>\n\n\n\n<ol class=\"wp-block-list\">\n<li><p><strong>Mobile app.<\/strong> Sends minimal events to your server: first open, registration, purchase, etc. No attribution SDK needed.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Click tracking endpoint.<\/strong> Receives clicks from ad campaigns. Records click data (device ID, campaign, timestamp) in the click store.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Attribution engine.<\/strong> When a first-open event arrives, queries the click store for matching clicks and runs the attribution logic.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Postback sender.<\/strong> Sends attribution results to ad networks via their postback URLs.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Click store.<\/strong> Fast key-value store (Redis) for click lookup. Indexed by device ID.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Implementation<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\">Click Tracking<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">When a user clicks an ad, the ad network redirects through your click tracking endpoint:<\/p>\n\n\n\n<pre><code class=\"language-typescript\">\/\/ GET \/track\/click?network=facebook&amp;campaign=CAMP-123&amp;device_id=...&amp;click_id=...\napp.get(&#39;\/track\/click&#39;, async (req, res) =&gt; {\n  const click: Click = {\n    id: generateId(),\n    networkId: req.query.network as string,\n    campaignId: req.query.campaign as string,\n    adGroupId: req.query.ad_group as string,\n    creativeId: req.query.creative as string,\n    deviceId: req.query.device_id as string,\n    clickId: req.query.click_id as string,\n    ip: req.ip,\n    userAgent: req.headers[&#39;user-agent&#39;] || &#39;&#39;,\n    timestamp: Date.now(),\n    referrer: req.query.referrer as string\n  };\n\n  \/\/ Store click\n  await clickStore.record(click);\n\n  \/\/ Redirect to app store or Universal Link\n  const redirectUrl = req.query.redirect_url as string;\n  res.redirect(302, redirectUrl);\n});\n<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">First Open Event<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">When the app opens for the first time, it sends a lightweight event to your server:<\/p>\n\n\n\n<pre><code class=\"language-swift\">\/\/ iOS: Send first open event\nfunc sendFirstOpenEvent() {\n    let event: [String: Any] = [\n        &quot;event&quot;: &quot;first_open&quot;,\n        &quot;device_id&quot;: getDeviceId(),\n        &quot;idfa&quot;: getIDFA(), \/\/ nil if ATT not consented\n        &quot;idfv&quot;: UIDevice.current.identifierForVendor?.uuidString ?? &quot;&quot;,\n        &quot;os&quot;: &quot;ios&quot;,\n        &quot;os_version&quot;: UIDevice.current.systemVersion,\n        &quot;app_version&quot;: Bundle.main.infoDictionary?[&quot;CFBundleShortVersionString&quot;] as? String ?? &quot;&quot;,\n        &quot;timestamp&quot;: Date().timeIntervalSince1970,\n        &quot;locale&quot;: Locale.current.identifier,\n        &quot;timezone&quot;: TimeZone.current.identifier\n    ]\n\n    URLSession.shared.dataTask(with: buildRequest(&quot;\/v1\/events&quot;, body: event)).resume()\n}\n<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">Attribution Engine<\/h3>\n\n\n\n<pre><code class=\"language-typescript\">interface AttributionResult {\n  attributed: boolean;\n  source: &#39;paid&#39; | &#39;organic&#39;;\n  networkId?: string;\n  campaignId?: string;\n  clickId?: string;\n  matchType: &#39;deterministic&#39; | &#39;probabilistic&#39; | &#39;organic&#39;;\n}\n\nasync function attributeInstall(event: InstallEvent): Promise&lt;AttributionResult&gt; {\n  \/\/ Step 1: Deterministic match (device ID)\n  if (event.deviceId) {\n    const clicks = await clickStore.getByDeviceId(event.deviceId, { within: &#39;7d&#39; });\n\n    if (clicks.length &gt; 0) {\n      const bestClick = selectBestClick(clicks); \/\/ Last-click model\n      const fraudCheck = await checkFraud(event, bestClick);\n\n      if (!fraudCheck.isFraud) {\n        return {\n          attributed: true,\n          source: &#39;paid&#39;,\n          networkId: bestClick.networkId,\n          campaignId: bestClick.campaignId,\n          clickId: bestClick.clickId,\n          matchType: &#39;deterministic&#39;\n        };\n      }\n    }\n  }\n\n  \/\/ Step 2: Probabilistic match (IP + User Agent)\n  const probClicks = await clickStore.getByIPAndUA(\n    event.ip,\n    event.userAgent,\n    { within: &#39;24h&#39; }\n  );\n\n  if (probClicks.length &gt; 0) {\n    const bestClick = selectBestClick(probClicks);\n    return {\n      attributed: true,\n      source: &#39;paid&#39;,\n      networkId: bestClick.networkId,\n      campaignId: bestClick.campaignId,\n      clickId: bestClick.clickId,\n      matchType: &#39;probabilistic&#39;\n    };\n  }\n\n  \/\/ Step 3: Organic\n  return {\n    attributed: false,\n    source: &#39;organic&#39;,\n    matchType: &#39;organic&#39;\n  };\n}\n<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">Postback Sending<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">After attribution, send results to the ad network:<\/p>\n\n\n\n<pre><code class=\"language-typescript\">async function sendPostback(result: AttributionResult, event: InstallEvent): Promise&lt;void&gt; {\n  if (!result.attributed || !result.networkId) return;\n\n  const network = await getNetworkConfig(result.networkId);\n  if (!network.postbackUrl) return;\n\n  const url = network.postbackUrl\n    .replace(&#39;{click_id}&#39;, result.clickId || &#39;&#39;)\n    .replace(&#39;{event}&#39;, &#39;install&#39;)\n    .replace(&#39;{timestamp}&#39;, String(event.timestamp))\n    .replace(&#39;{device_id}&#39;, event.deviceId || &#39;&#39;);\n\n  try {\n    const response = await fetch(url, { method: &#39;GET&#39; });\n    await logPostback(result, response.status);\n  } catch (error) {\n    await logPostbackError(result, error);\n    \/\/ Queue for retry\n    await retryQueue.add({ url, result, attempt: 1 });\n  }\n}\n<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\">Self-Attributing Network Integration<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">SANs (Meta, Google, TikTok) require special S2S integration because they do not use standard click tracking:<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Meta Conversions API<\/h3>\n\n\n\n<pre><code class=\"language-typescript\">async function sendMetaConversion(event: InstallEvent): Promise&lt;void&gt; {\n  const payload = {\n    data: [{\n      event_name: &#39;app_install&#39;,\n      event_time: Math.floor(event.timestamp \/ 1000),\n      user_data: {\n        client_ip_address: event.ip,\n        client_user_agent: event.userAgent,\n        madid: event.deviceId \/\/ Mobile Advertiser ID\n      },\n      app_data: {\n        application_tracking_enabled: event.attConsented ? 1 : 0,\n        advertiser_tracking_enabled: event.attConsented ? 1 : 0\n      }\n    }]\n  };\n\n  await fetch(\n    `https:\/\/graph.facebook.com\/v18.0\/${PIXEL_ID}\/events?access_token=${ACCESS_TOKEN}`,\n    {\n      method: &#39;POST&#39;,\n      headers: { &#39;Content-Type&#39;: &#39;application\/json&#39; },\n      body: JSON.stringify(payload)\n    }\n  );\n}\n<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">Google Measurement Protocol<\/h3>\n\n\n\n<pre><code class=\"language-typescript\">async function sendGoogleConversion(event: InstallEvent): Promise&lt;void&gt; {\n  const payload = {\n    client_id: event.deviceId,\n    events: [{\n      name: &#39;first_open&#39;,\n      params: {\n        campaign: event.attributedCampaign,\n        source: &#39;google&#39;,\n        medium: &#39;cpc&#39;\n      }\n    }]\n  };\n\n  await fetch(\n    `https:\/\/www.google-analytics.com\/mp\/collect?measurement_id=${GA_ID}&amp;api_secret=${API_SECRET}`,\n    {\n      method: &#39;POST&#39;,\n      body: JSON.stringify(payload)\n    }\n  );\n}\n<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\">Data Storage<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\">Click Store Schema<\/h3>\n\n\n\n<pre><code class=\"language-sql\">CREATE TABLE clicks (\n  id TEXT PRIMARY KEY,\n  network_id TEXT NOT NULL,\n  campaign_id TEXT,\n  ad_group_id TEXT,\n  creative_id TEXT,\n  device_id TEXT,\n  click_id TEXT,\n  ip_address TEXT,\n  user_agent TEXT,\n  timestamp BIGINT NOT NULL,\n  INDEX idx_device_id (device_id),\n  INDEX idx_ip_ua (ip_address, user_agent),\n  INDEX idx_timestamp (timestamp)\n);\n\n-- TTL: Delete clicks older than 30 days\n<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">Attribution Log Schema<\/h3>\n\n\n\n<pre><code class=\"language-sql\">CREATE TABLE attributions (\n  id TEXT PRIMARY KEY,\n  device_id TEXT,\n  event_type TEXT NOT NULL,\n  attributed BOOLEAN NOT NULL,\n  source TEXT NOT NULL, -- &#39;paid&#39; or &#39;organic&#39;\n  network_id TEXT,\n  campaign_id TEXT,\n  click_id TEXT,\n  match_type TEXT, -- &#39;deterministic&#39;, &#39;probabilistic&#39;, &#39;organic&#39;\n  event_timestamp BIGINT NOT NULL,\n  attribution_timestamp BIGINT NOT NULL,\n  postback_sent BOOLEAN DEFAULT FALSE,\n  INDEX idx_device (device_id),\n  INDEX idx_campaign (campaign_id)\n);\n<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\">Tolinku for S2S Attribution<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\"><a href=\"https:\/\/tolinku.com\/features\/analytics\">Tolinku<\/a> provides server-side click tracking and attribution via its <a href=\"https:\/\/tolinku.com\/docs\/developer\/api-reference\/\">API<\/a>. Deep link clicks are tracked server-side, and attribution data is available through the dashboard and API. This gives you S2S attribution for deep link campaigns without building the full infrastructure.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">For mobile attribution, see <a href=\"https:\/\/tolinku.com\/blog\/mobile-attribution-developers-guide\/\">mobile attribution: a developer&#39;s guide<\/a>. For server-side deferred matching, see <a href=\"https:\/\/tolinku.com\/blog\/server-side-deferred-matching\/\">server-side deferred matching: architecture guide<\/a>.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Implement server-to-server attribution for mobile apps. Build secure, privacy-compliant attribution without client-side SDK dependencies.<\/p>\n","protected":false},"author":2,"featured_media":1754,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"rank_math_title":"Server-to-Server Attribution: Architecture Guide","rank_math_description":"Implement server-to-server attribution for mobile apps. Build secure, privacy-compliant attribution without client-side SDK dependencies.","rank_math_focus_keyword":"server-to-server 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-server-to-server-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-server-to-server-attribution.png","footnotes":""},"categories":[14],"tags":[37,62,305,28,20,69,36,333],"class_list":["post-1755","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-analytics","tag-analytics","tag-api","tag-architecture","tag-attribution","tag-deep-linking","tag-mobile-development","tag-privacy","tag-server-side"],"_links":{"self":[{"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/posts\/1755","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=1755"}],"version-history":[{"count":3,"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/posts\/1755\/revisions"}],"predecessor-version":[{"id":2699,"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/posts\/1755\/revisions\/2699"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/media\/1754"}],"wp:attachment":[{"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/media?parent=1755"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/categories?post=1755"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/tags?post=1755"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}