{"id":1731,"date":"2026-07-12T17:00:00","date_gmt":"2026-07-12T22:00:00","guid":{"rendered":"https:\/\/tolinku.com\/blog\/?p=1731"},"modified":"2026-03-07T03:50:05","modified_gmt":"2026-03-07T08:50:05","slug":"postback-urls-attribution","status":"publish","type":"post","link":"https:\/\/tolinku.com\/blog\/postback-urls-attribution\/","title":{"rendered":"Postback URLs for Mobile Attribution: How They Work"},"content":{"rendered":"\n<p class=\"wp-block-paragraph\">A postback URL is a server-to-server callback that fires when a conversion event occurs. When a user installs your app, your attribution system sends an HTTP request to the ad network&#39;s postback URL with the install details: campaign ID, device identifier, timestamp, and any custom data. The ad network uses this data to optimize its campaigns and report conversions.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">This guide covers how postback URLs work in mobile attribution. For the install attribution flow, see <a href=\"https:\/\/tolinku.com\/blog\/install-attribution-flow\/\">install attribution flow: from ad click to first open<\/a>. For webhook integrations, see <a href=\"https:\/\/tolinku.com\/blog\/webhooks-integrations-deep-linking\/\">webhooks and integrations for deep linking<\/a>.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">How Postback URLs Work<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\">The Flow<\/h3>\n\n\n\n<pre><code>1. User clicks ad on Ad Network A\n2. Ad Network A records click with click_id=ABC123\n3. User installs and opens app\n4. Attribution provider determines Ad Network A drove the install\n5. Attribution provider sends HTTP GET\/POST to Ad Network A&#39;s postback URL:\n   https:\/\/postback.adnetwork-a.com\/install?click_id=ABC123&app_id=com.yourapp&event=install\n6. Ad Network A records the conversion and optimizes future ad delivery\n<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">Postback URL Structure<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">A typical postback URL uses macro placeholders that the attribution provider replaces with actual values:<\/p>\n\n\n\n<pre><code>https:\/\/postback.adnetwork.com\/conversion?\n  click_id={click_id}&amp;\n  app_id={app_id}&amp;\n  event_name={event_name}&amp;\n  event_time={event_time}&amp;\n  device_id={device_id}&amp;\n  revenue={revenue}&amp;\n  currency={currency}\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The macros (values in curly braces) are replaced at send time:<\/p>\n\n\n\n<pre><code>https:\/\/postback.adnetwork.com\/conversion?\n  click_id=ABC123&amp;\n  app_id=com.yourapp&amp;\n  event_name=install&amp;\n  event_time=1720780200&amp;\n  device_id=hashed_gaid&amp;\n  revenue=0&amp;\n  currency=USD\n<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\">Common Postback Macros<\/h2>\n\n\n\n<figure class=\"wp-block-table\"><table>\n<thead>\n<tr>\n<th>Macro<\/th>\n<th>Description<\/th>\n<th>Example<\/th>\n<\/tr>\n<\/thead>\n<tbody><tr>\n<td><code>{click_id}<\/code><\/td>\n<td>Ad network&#39;s click identifier<\/td>\n<td><code>ABC123<\/code><\/td>\n<\/tr>\n<tr>\n<td><code>{app_id}<\/code><\/td>\n<td>App bundle ID \/ package name<\/td>\n<td><code>com.yourapp<\/code><\/td>\n<\/tr>\n<tr>\n<td><code>{event_name}<\/code><\/td>\n<td>Conversion event name<\/td>\n<td><code>install<\/code>, <code>purchase<\/code>, <code>signup<\/code><\/td>\n<\/tr>\n<tr>\n<td><code>{event_time}<\/code><\/td>\n<td>Event timestamp (Unix)<\/td>\n<td><code>1720780200<\/code><\/td>\n<\/tr>\n<tr>\n<td><code>{device_id}<\/code><\/td>\n<td>Device advertising ID (hashed)<\/td>\n<td><code>sha256_of_gaid<\/code><\/td>\n<\/tr>\n<tr>\n<td><code>{idfa}<\/code><\/td>\n<td>iOS Identifier for Advertisers<\/td>\n<td><code>A1B2C3D4-...<\/code><\/td>\n<\/tr>\n<tr>\n<td><code>{gaid}<\/code><\/td>\n<td>Google Advertising ID<\/td>\n<td><code>a1b2c3d4-...<\/code><\/td>\n<\/tr>\n<tr>\n<td><code>{revenue}<\/code><\/td>\n<td>Revenue amount<\/td>\n<td><code>9.99<\/code><\/td>\n<\/tr>\n<tr>\n<td><code>{currency}<\/code><\/td>\n<td>Revenue currency<\/td>\n<td><code>USD<\/code><\/td>\n<\/tr>\n<tr>\n<td><code>{campaign_id}<\/code><\/td>\n<td>Campaign identifier<\/td>\n<td><code>CAMP-789<\/code><\/td>\n<\/tr>\n<tr>\n<td><code>{ad_group_id}<\/code><\/td>\n<td>Ad group identifier<\/td>\n<td><code>AG-456<\/code><\/td>\n<\/tr>\n<tr>\n<td><code>{creative_id}<\/code><\/td>\n<td>Creative identifier<\/td>\n<td><code>CR-123<\/code><\/td>\n<\/tr>\n<tr>\n<td><code>{country}<\/code><\/td>\n<td>Install country (ISO 3166)<\/td>\n<td><code>US<\/code><\/td>\n<\/tr>\n<tr>\n<td><code>{os_version}<\/code><\/td>\n<td>Device OS version<\/td>\n<td><code>17.5<\/code><\/td>\n<\/tr>\n<tr>\n<td><code>{ip}<\/code><\/td>\n<td>User&#39;s IP address<\/td>\n<td><code>203.0.113.42<\/code><\/td>\n<\/tr>\n<\/tbody><\/table><\/figure>\n\n\n\n<h2 class=\"wp-block-heading\">Types of Postbacks<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\">Install Postback<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Fires when the app is installed and opened for the first time:<\/p>\n\n\n\n<pre><code>https:\/\/postback.adnetwork.com\/install?\n  click_id={click_id}&amp;\n  event=install&amp;\n  event_time={event_time}&amp;\n  device_id={device_id}\n<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">In-App Event Postback<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Fires when the user completes a specific in-app event (purchase, registration, level completion):<\/p>\n\n\n\n<pre><code>https:\/\/postback.adnetwork.com\/event?\n  click_id={click_id}&amp;\n  event={event_name}&amp;\n  event_time={event_time}&amp;\n  revenue={revenue}&amp;\n  currency={currency}\n<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">Revenue Postback<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">A specialized in-app event postback that includes revenue data for ROAS (Return on Ad Spend) optimization:<\/p>\n\n\n\n<pre><code>https:\/\/postback.adnetwork.com\/revenue?\n  click_id={click_id}&amp;\n  event=purchase&amp;\n  revenue=49.99&amp;\n  currency=USD&amp;\n  product_id=premium_annual\n<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">Rejection Postback<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Fires when an install claim is rejected (fraud, duplicate, wrong attribution):<\/p>\n\n\n\n<pre><code>https:\/\/postback.adnetwork.com\/reject?\n  click_id={click_id}&amp;\n  rejection_reason=fraud_detected&amp;\n  event_time={event_time}\n<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\">Configuring Postback URLs<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\">Setting Up a Postback<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Each ad network provides its own postback URL template. You configure this in your attribution provider&#39;s dashboard:<\/p>\n\n\n\n<pre><code class=\"language-json\">{\n  &quot;network&quot;: &quot;Ad Network A&quot;,\n  &quot;postback_url&quot;: &quot;https:\/\/postback.adnetwork-a.com\/conversion?click_id={click_id}&amp;event={event_name}&amp;revenue={revenue}&quot;,\n  &quot;events&quot;: [&quot;install&quot;, &quot;purchase&quot;, &quot;registration&quot;],\n  &quot;send_for&quot;: &quot;attributed_only&quot;,\n  &quot;method&quot;: &quot;GET&quot;\n}\n<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">Event Mapping<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Map your in-app events to the ad network&#39;s expected event names:<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table>\n<thead>\n<tr>\n<th>Your Event<\/th>\n<th>Ad Network Event<\/th>\n<th>Postback<\/th>\n<\/tr>\n<\/thead>\n<tbody><tr>\n<td><code>first_open<\/code><\/td>\n<td><code>install<\/code><\/td>\n<td>Install postback<\/td>\n<\/tr>\n<tr>\n<td><code>registration_complete<\/code><\/td>\n<td><code>registration<\/code><\/td>\n<td>Event postback<\/td>\n<\/tr>\n<tr>\n<td><code>first_purchase<\/code><\/td>\n<td><code>purchase<\/code><\/td>\n<td>Revenue postback<\/td>\n<\/tr>\n<tr>\n<td><code>subscription_start<\/code><\/td>\n<td><code>subscribe<\/code><\/td>\n<td>Revenue postback<\/td>\n<\/tr>\n<tr>\n<td><code>tutorial_complete<\/code><\/td>\n<td><code>tutorial<\/code><\/td>\n<td>Event postback<\/td>\n<\/tr>\n<\/tbody><\/table><\/figure>\n\n\n\n<h2 class=\"wp-block-heading\">Building Your Own Postback System<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">If you are building attribution in-house, here is how to implement postback sending:<\/p>\n\n\n\n<pre><code class=\"language-typescript\">interface PostbackConfig {\n  networkId: string;\n  urlTemplate: string;\n  events: string[];\n  method: &#39;GET&#39; | &#39;POST&#39;;\n  retryPolicy: { maxRetries: number; backoffMs: number };\n}\n\nasync function sendPostback(\n  config: PostbackConfig,\n  eventData: Record&lt;string, string&gt;\n): Promise&lt;void&gt; {\n  \/\/ Replace macros in URL template\n  let url = config.urlTemplate;\n  for (const [key, value] of Object.entries(eventData)) {\n    url = url.replace(`{${key}}`, encodeURIComponent(value));\n  }\n\n  \/\/ Remove any unreplaced macros\n  url = url.replace(\/\\{[^}]+\\}\/g, &#39;&#39;);\n\n  for (let attempt = 0; attempt &lt;= config.retryPolicy.maxRetries; attempt++) {\n    try {\n      const response = await fetch(url, { method: config.method });\n\n      if (response.ok) {\n        console.log(`Postback sent to ${config.networkId}: ${response.status}`);\n        return;\n      }\n\n      if (response.status &gt;= 500) {\n        \/\/ Server error, retry\n        await sleep(config.retryPolicy.backoffMs * Math.pow(2, attempt));\n        continue;\n      }\n\n      \/\/ Client error (4xx), don&#39;t retry\n      console.error(`Postback failed for ${config.networkId}: ${response.status}`);\n      return;\n    } catch (error) {\n      if (attempt &lt; config.retryPolicy.maxRetries) {\n        await sleep(config.retryPolicy.backoffMs * Math.pow(2, attempt));\n      }\n    }\n  }\n\n  console.error(`Postback to ${config.networkId} failed after ${config.retryPolicy.maxRetries} retries`);\n}\n<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\">Postback Validation<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\">Verifying Incoming Postbacks<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">If you receive postbacks (e.g., from ad networks reporting click data), validate them:<\/p>\n\n\n\n<pre><code class=\"language-typescript\">function validatePostback(request: Request, networkSecret: string): boolean {\n  \/\/ Check signature\n  const signature = request.headers.get(&#39;X-Signature&#39;);\n  const body = request.body;\n  const expectedSignature = hmacSHA256(body, networkSecret);\n\n  if (signature !== expectedSignature) {\n    return false;\n  }\n\n  \/\/ Check timestamp freshness (prevent replay attacks)\n  const timestamp = parseInt(request.url.searchParams.get(&#39;event_time&#39;) || &#39;0&#39;);\n  const now = Math.floor(Date.now() \/ 1000);\n\n  if (Math.abs(now - timestamp) &gt; 3600) {\n    return false; \/\/ More than 1 hour old\n  }\n\n  return true;\n}\n<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">Common Postback Failures<\/h3>\n\n\n\n<figure class=\"wp-block-table\"><table>\n<thead>\n<tr>\n<th>Issue<\/th>\n<th>Cause<\/th>\n<th>Fix<\/th>\n<\/tr>\n<\/thead>\n<tbody><tr>\n<td>404 response<\/td>\n<td>Wrong postback URL<\/td>\n<td>Verify URL with ad network<\/td>\n<\/tr>\n<tr>\n<td>Missing click_id<\/td>\n<td>Macro not replaced<\/td>\n<td>Check macro syntax matches provider&#39;s format<\/td>\n<\/tr>\n<tr>\n<td>Duplicate postbacks<\/td>\n<td>Retry logic without dedup<\/td>\n<td>Add idempotency keys<\/td>\n<\/tr>\n<tr>\n<td>Delayed postbacks<\/td>\n<td>Queue backlog<\/td>\n<td>Monitor queue depth, scale workers<\/td>\n<\/tr>\n<tr>\n<td>Empty revenue<\/td>\n<td>Revenue macro not mapped<\/td>\n<td>Map revenue events correctly<\/td>\n<\/tr>\n<\/tbody><\/table><\/figure>\n\n\n\n<h2 class=\"wp-block-heading\">Privacy and Postbacks<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\">ATT and IDFA<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">On iOS, if the user has not consented to tracking via <a href=\"https:\/\/developer.apple.com\/documentation\/apptrackingtransparency\" rel=\"nofollow noopener\" target=\"_blank\">ATT<\/a>, the IDFA is unavailable. Postbacks must use alternative identifiers:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>SKAdNetwork postbacks:<\/strong> Apple sends postbacks directly to the ad network with campaign-level (not user-level) data.<\/li>\n<li><strong>Hashed identifiers:<\/strong> Some providers use hashed email or phone as an alternative identifier.<\/li>\n<li><strong>No identifier:<\/strong> Some postbacks omit device identifiers entirely and rely on the click_id alone.<\/li>\n<\/ul>\n\n\n\n<h3 class=\"wp-block-heading\">GDPR<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Under GDPR, sending user data (device IDs, IP addresses) to third-party ad networks requires a legal basis. Ensure your privacy policy covers this data sharing, and provide users with opt-out mechanisms.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Tolinku for Attribution Callbacks<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\"><a href=\"https:\/\/tolinku.com\/features\/webhooks\">Tolinku&#39;s webhooks<\/a> can be used as postback URLs for deep link attribution events. When a user clicks a Tolinku deep link and converts, Tolinku can send a webhook to your server with the attribution data. Configure webhooks in the <a href=\"https:\/\/tolinku.com\/docs\/developer\/webhooks\/\">Tolinku dashboard<\/a>.<\/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 the full install attribution flow, see <a href=\"https:\/\/tolinku.com\/blog\/install-attribution-flow\/\">install attribution flow: from ad click to first open<\/a>.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Understand postback URLs in mobile attribution. Configure server-to-server callbacks for install events, conversions, and campaign data.<\/p>\n","protected":false},"author":2,"featured_media":1730,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"rank_math_title":"Postback URLs for Mobile Attribution: How They Work","rank_math_description":"Understand postback URLs in mobile attribution. Configure server-to-server callbacks for install events, conversions, and campaign data.","rank_math_focus_keyword":"postback URLs 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-postback-urls-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-postback-urls-attribution.png","footnotes":""},"categories":[14],"tags":[37,62,28,20,69,523,524,61],"class_list":["post-1731","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-analytics","tag-analytics","tag-api","tag-attribution","tag-deep-linking","tag-mobile-development","tag-postback","tag-server-to-server","tag-webhooks"],"_links":{"self":[{"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/posts\/1731","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=1731"}],"version-history":[{"count":3,"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/posts\/1731\/revisions"}],"predecessor-version":[{"id":2690,"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/posts\/1731\/revisions\/2690"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/media\/1730"}],"wp:attachment":[{"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/media?parent=1731"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/categories?post=1731"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/tags?post=1731"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}