<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Kamyar Taher</title>
    <description>The latest articles on DEV Community by Kamyar Taher (@kamyartaher).</description>
    <link>https://dev.to/kamyartaher</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4222465%2Faa3a2d50-ea0c-4ba8-8f09-fa7d960409dc.png</url>
      <title>DEV Community: Kamyar Taher</title>
      <link>https://dev.to/kamyartaher</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/kamyartaher"/>
    <language>en</language>
    <item>
      <title>RevenueCat attribution: join subscription revenue to campaigns</title>
      <dc:creator>Kamyar Taher</dc:creator>
      <pubDate>Mon, 02 Nov 2026 16:33:37 +0000</pubDate>
      <link>https://dev.to/attrikit/revenuecat-attribution-join-subscription-revenue-to-campaigns-dcc</link>
      <guid>https://dev.to/attrikit/revenuecat-attribution-join-subscription-revenue-to-campaigns-dcc</guid>
      <description>&lt;p&gt;&lt;em&gt;This article was drafted with AI assistance and checked against the sources listed.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Key takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;RevenueCat is not an attribution network: apart from its own Apple Search Ads integration, it sends revenue events to attribution providers rather than attributing installs to ad campaigns.&lt;/li&gt;
&lt;li&gt;A RevenueCat purchase joins an install through the App User ID, which the app must also report to its attribution SDK at launch and after every logIn.&lt;/li&gt;
&lt;li&gt;RevenueCat reports a refund as a CANCELLATION event with a negative price, not as a separate refund event type.&lt;/li&gt;
&lt;li&gt;A RevenueCat webhook endpoint should answer 200 within 60 seconds; otherwise RevenueCat retries up to 5 times over about 80 minutes with the same event id.&lt;/li&gt;
&lt;li&gt;RevenueCat's own Apple Search Ads integration stores campaign, ad group and keyword, and groups other installs as Organic.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;RevenueCat attribution means joining each purchase RevenueCat reports to the install that produced it, and through that install to the ad campaign. RevenueCat knows the purchase and the App User ID, the identifier it keeps for each customer. Your attribution tool knows the install and its campaign. The join works when the app reports the same App User ID to both, and RevenueCat's webhook delivers every purchase, renewal and refund to the attribution tool.&lt;/p&gt;

&lt;p&gt;This guide covers what the webhook sends, how to authenticate it, how the join works, and what renewals and refunds do to revenue. It ends with a step-by-step RevenueCat connector setup. A connector here means a webhook endpoint plus one SDK call that reports the App User ID.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why RevenueCat does not know most campaigns
&lt;/h2&gt;

&lt;p&gt;RevenueCat's documentation states its role plainly: "RevenueCat isn't an attribution network." Its attribution integrations send subscription and revenue events from RevenueCat's servers to a provider. The provider then matches those events to its own users and campaigns. The list includes Adjust, AppsFlyer, Branch, Kochava, Singular and Tenjin.&lt;/p&gt;

&lt;p&gt;Apple Search Ads is the one exception. After the app calls &lt;code&gt;enableAdServicesAttributionTokenCollection()&lt;/code&gt;, RevenueCat fetches attribution data from Apple through AdServices and stores the campaign, ad group and keyword. This needs iOS 14.3 or later, and RevenueCat says to allow up to 7 days for the data.&lt;/p&gt;

&lt;p&gt;Transactions not attributed to an Apple Search Ads campaign are grouped as Organic in that integration (RevenueCat also shows Unspecified for Apple Ads Basic accounts). An install that came from a Meta or TikTok ad therefore lands in Organic there. For those networks, the campaign has to come from the tool that saw the click, and RevenueCat's events have to reach that tool.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a RevenueCat webhook sends
&lt;/h2&gt;

&lt;p&gt;A RevenueCat webhook is an HTTPS POST that RevenueCat sends to a URL you configure each time a customer event happens. Each request carries one event. These types change revenue:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Event type&lt;/th&gt;
&lt;th&gt;RevenueCat's definition&lt;/th&gt;
&lt;th&gt;Effect on revenue&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;INITIAL_PURCHASE&lt;/td&gt;
&lt;td&gt;A new subscription was purchased&lt;/td&gt;
&lt;td&gt;First sale, or a trial start at price 0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RENEWAL&lt;/td&gt;
&lt;td&gt;A subscription renewed, or a lapsed user resubscribed&lt;/td&gt;
&lt;td&gt;A recurring sale&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;NON_RENEWING_PURCHASE&lt;/td&gt;
&lt;td&gt;A purchase that won't auto-renew&lt;/td&gt;
&lt;td&gt;A one-off sale&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CANCELLATION&lt;/td&gt;
&lt;td&gt;A subscription or purchase was canceled or refunded&lt;/td&gt;
&lt;td&gt;A refund when the price is negative&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;REFUND_REVERSED&lt;/td&gt;
&lt;td&gt;A refund was reversed&lt;/td&gt;
&lt;td&gt;The sale counts again (App Store only)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TRANSFER&lt;/td&gt;
&lt;td&gt;Purchases moved between App User IDs&lt;/td&gt;
&lt;td&gt;None, but it moves the join&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A few fields carry the attribution. &lt;code&gt;app_user_id&lt;/code&gt; is the last App User ID seen for the customer, &lt;code&gt;original_app_user_id&lt;/code&gt; the first, and &lt;code&gt;aliases&lt;/code&gt; every one ever used. &lt;code&gt;app_id&lt;/code&gt; is the public identifier of the RevenueCat app the event belongs to.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;price&lt;/code&gt; is the transaction in US dollars and &lt;code&gt;price_in_purchased_currency&lt;/code&gt; is what the customer paid. Both are 0 for free trials, negative for refunds, and null when unknown. &lt;code&gt;environment&lt;/code&gt; is &lt;code&gt;SANDBOX&lt;/code&gt; or &lt;code&gt;PRODUCTION&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Two timestamps differ. &lt;code&gt;purchased_at_ms&lt;/code&gt; is when the transaction happened, while &lt;code&gt;event_timestamp_ms&lt;/code&gt; is when RevenueCat generated the event. Date revenue by the purchase time, or a delayed delivery moves a sale to another day.&lt;/p&gt;

&lt;h2&gt;
  
  
  Authenticating the webhook and handling retries
&lt;/h2&gt;

&lt;p&gt;RevenueCat offers two checks. The first, and the one it recommends, is the Authorization header. You type a secret value into the webhook settings, RevenueCat sends it on every request, and your server refuses any request without it.&lt;/p&gt;

&lt;p&gt;The second is HMAC signing, which RevenueCat offers for stronger verification. Each request then carries an &lt;code&gt;X-RevenueCat-Webhook-Signature&lt;/code&gt; header shaped &lt;code&gt;t=&amp;lt;timestamp&amp;gt;,v1=&amp;lt;signature&amp;gt;&lt;/code&gt;. The signature is an HMAC-SHA256, a keyed hash, of the timestamp, a dot and the raw request body, keyed with the integration's signing secret. RevenueCat suggests refusing a timestamp more than about 5 minutes from your clock, which stops replayed requests.&lt;/p&gt;

&lt;p&gt;Your endpoint should answer with status 200, and RevenueCat disconnects after 60 seconds. Any other status counts as a failure. RevenueCat then retries up to 5 times, after about 2.5, 5, 10, 20 and 40 minutes, roughly 80 minutes in all.&lt;/p&gt;

&lt;p&gt;Retries reuse the same event &lt;code&gt;id&lt;/code&gt;. A consumer that stores each id and ignores a second copy never counts a sale twice, which is what RevenueCat advises.&lt;/p&gt;

&lt;h2&gt;
  
  
  How a purchase joins an install
&lt;/h2&gt;

&lt;p&gt;The join key is the App User ID. When the RevenueCat SDK is configured without one, it generates a random ID prefixed with &lt;code&gt;$RCAnonymousID:&lt;/code&gt; and stores it on the device. When the app later calls &lt;code&gt;logIn&lt;/code&gt; with your own ID, RevenueCat can alias the anonymous ID to it. Webhooks then carry your ID as &lt;code&gt;app_user_id&lt;/code&gt; and the anonymous one as &lt;code&gt;original_app_user_id&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The attribution tool holds the other half. Its SDK reports the &lt;a href="https://attrikit.io/en/glossary/first-open" rel="noopener noreferrer"&gt;first open&lt;/a&gt;, which it ties to a campaign click, so the install already has a campaign. On Android, a click identifier can travel through Google Play in a tagged link's &lt;a href="https://attrikit.io/en/blog/google-play-install-referrer" rel="noopener noreferrer"&gt;install referrer&lt;/a&gt;. For the two records to meet, the app hands the attribution SDK the value of &lt;code&gt;Purchases.shared.appUserID&lt;/code&gt; at launch, and again after every &lt;code&gt;logIn&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;When a webhook arrives, the tool looks up &lt;code&gt;app_user_id&lt;/code&gt;, then &lt;code&gt;original_app_user_id&lt;/code&gt;, then each alias. The purchase inherits the campaign of the install it finds. That order covers a purchase made after login by someone who first opened the app anonymously.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2i20smqcgo3b2zadsufe.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2i20smqcgo3b2zadsufe.webp" alt="The join: a campaign click and its install on one side, RevenueCat purchase events and their webhook on the other, joined on the App User ID so each purchase inherits the install's campaign." width="800" height="1680"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Both sides must hold the same App User ID; the webhook's IDs are tried in order.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Apple's SKAdNetwork cannot stand in for this join. Its postbacks go to the ad network, carry no user or device data in their signed part and arrive days later, so they describe a campaign's installs without naming any one of them.&lt;/p&gt;

&lt;p&gt;Two details break the join if ignored. A reinstall clears RevenueCat's local cache, so an anonymous user gets a new ID on the new install. RevenueCat also advises against email addresses as App User IDs and recommends a random UUID, because customer status is readable through its public API.&lt;/p&gt;

&lt;h2&gt;
  
  
  Renewals, refunds and test purchases
&lt;/h2&gt;

&lt;p&gt;Each RENEWAL event names the same customer as the first purchase, so it joins the same install. A campaign's revenue therefore keeps growing after its spend has stopped.&lt;/p&gt;

&lt;p&gt;Refunds have no event type of their own. RevenueCat reports a refund as a CANCELLATION with a negative price, and a refund made through Apple support carries the cancel reason &lt;code&gt;CUSTOMER_SUPPORT&lt;/code&gt;. The event fires only when the latest subscription period is refunded, not an earlier one.&lt;/p&gt;

&lt;p&gt;Book a refund as negative revenue against the sale it reverses, instead of deleting the sale. The history stays auditable, and a REFUND_REVERSED event can restore the amount later.&lt;/p&gt;

&lt;p&gt;Test traffic needs filtering. Each webhook can send production events, sandbox events or both, and a TEST event arrives when you issue a test webhook from the dashboard. A sandbox purchase proves the pipeline works, but it must never count as revenue or reach an ad network as a conversion.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Do not filter out cancellations&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;RevenueCat lets a webhook send only some event types. A filter that drops CANCELLATION also drops every refund, and campaign revenue then stays too high.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Setting up a RevenueCat connector, step by step
&lt;/h2&gt;

&lt;p&gt;The steps below use the AttriKit RevenueCat connector, documented in full in &lt;a href="https://attrikit.io/en/docs/integrations/revenuecat" rel="noopener noreferrer"&gt;the RevenueCat integration guide&lt;/a&gt;. They take one sitting if the iOS SDK already reports first opens.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; &lt;strong&gt;Copy the webhook endpoint.&lt;/strong&gt; On the Integrations page of the dashboard, the RevenueCat card shows the shared webhook endpoint, &lt;code&gt;https://attrikit.io/v1/webhooks/revenuecat&lt;/code&gt;. If the card shows none, write to &lt;a href="mailto:contact@clashware.com"&gt;contact@clashware.com&lt;/a&gt; before you continue.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Add the webhook in RevenueCat.&lt;/strong&gt; In your RevenueCat project, open Integrations, then Webhooks, and add a configuration. Paste the URL, set a long and unique Authorization value, and send both production and sandbox events so a sandbox purchase can test the setup.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Enter the public app ID.&lt;/strong&gt; Copy the public app ID from RevenueCat's project settings, not the &lt;code&gt;appl_&lt;/code&gt; SDK API key. Enter it on the RevenueCat card with the same Authorization value. The connector finds your workspace from each event's &lt;code&gt;app_id&lt;/code&gt; and refuses requests that carry neither that value nor a valid signature.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Turn on signing.&lt;/strong&gt; Enable HMAC webhook signing on the same RevenueCat webhook, then save the secret it shows as &lt;code&gt;signing_secret&lt;/code&gt; through the Management API. A delivery is accepted on the Authorization value or on a valid signature, and a signature more than five minutes old is refused.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Report the App User ID from the app.&lt;/strong&gt; Call the identify method at launch and after every successful &lt;code&gt;logIn&lt;/code&gt;, as in the code below.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Watch the first event arrive.&lt;/strong&gt; Make a sandbox purchase. Within seconds it appears on the Live events page with the source revenuecat.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Make RevenueCat the revenue sender.&lt;/strong&gt; In the Event senders card, assign Purchases, Subscriptions and Refunds to RevenueCat (and Trials, if the app reports trial starts only through RevenueCat), so ad networks receive each revenue event exactly once. If the app reports purchases only through RevenueCat and another sender is chosen, those events never reach the network.
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="kt"&gt;Purchases&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;configure&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;withAPIKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"appl_your_key"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="kt"&gt;AttriKit&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;start&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;apiKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"pk_your_key"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;consent&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;measurementGranted&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="kt"&gt;AttriKit&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setUserID&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Purchases&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;shared&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;appUserID&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;// After Purchases.shared.logIn(...) succeeds:&lt;/span&gt;
&lt;span class="kt"&gt;AttriKit&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setUserID&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Purchases&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;shared&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;appUserID&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;a href="https://attrikit.io/en/docs/sdk-ios" rel="noopener noreferrer"&gt;iOS SDK guide&lt;/a&gt; covers the &lt;code&gt;start&lt;/code&gt; call and consent.&lt;/p&gt;

&lt;h2&gt;
  
  
  Limits and edge cases
&lt;/h2&gt;

&lt;p&gt;A purchase can arrive before the app has reported any App User ID. The connector holds it as deferred and reconciles it once the app reports one of the customer's IDs, so it is neither dropped nor counted twice.&lt;/p&gt;

&lt;p&gt;The webhook has no dedicated ad network or campaign field. Its optional &lt;code&gt;subscriber_attributes&lt;/code&gt; map carries attribution values only if your app wrote them to RevenueCat, so the join uses the install's own attribution, organic included.&lt;/p&gt;

&lt;p&gt;Four kinds of event are stored and shown in Live events but never count as revenue or go to a network. They are sandbox purchases, RevenueCat Test Store purchases, entitlements granted from the RevenueCat dashboard, and Family Sharing access to another member's purchase.&lt;/p&gt;

&lt;p&gt;When RevenueCat transfers purchases to another App User ID, that ID inherits the install the original ID was joined to, unless it is already joined to one. A refund is booked as negative revenue against its sale, and a reversed refund counts again without being sent to an ad network.&lt;/p&gt;

&lt;p&gt;A custom App User ID containing @, or longer than 256 bytes, is refused and creates no join. RevenueCat's guidance is stricter, at 100 characters, so following it keeps an ID inside both.&lt;/p&gt;

&lt;p&gt;Each sale is kept in the currency the customer paid and in RevenueCat's US dollar figure. An app selling in several storefront currencies sees its revenue added up in US dollars, when RevenueCat supplied that figure for every sale in the period.&lt;/p&gt;

&lt;p&gt;Before you trust campaign revenue&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The webhook carries your Authorization value, or a valid signature, and anything else is refused.&lt;/li&gt;
&lt;li&gt;RevenueCat sends both production and sandbox events, and sandbox purchases never count as revenue.&lt;/li&gt;
&lt;li&gt;No filter drops CANCELLATION, so refunds reach the campaign.&lt;/li&gt;
&lt;li&gt;The app reports the App User ID at launch and again after every &lt;code&gt;logIn&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;One sandbox purchase lands on an install you can name.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Which setup you need
&lt;/h2&gt;

&lt;p&gt;If Apple Search Ads is your only paid channel, RevenueCat's own integration gives campaign, ad group and keyword for installs Apple attributes. Apple Ads Basic accounts return Unspecified for those fields, and age or gender targeting can return no attribution. Once you buy installs on Meta, TikTok or any other network, you need an attribution tool that recorded the click and receives the RevenueCat webhook. Check the join first: make one sandbox purchase and confirm it lands on an install you can name.&lt;/p&gt;

&lt;p&gt;Try it on your own app.&lt;/p&gt;

&lt;p&gt;Your first 10,000 qualified installs are free, one time, with no card required.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://attrikit.io/en/sign-up" rel="noopener noreferrer"&gt;Create your free workspace&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Questions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  What value goes in the RevenueCat webhook Authorization header?
&lt;/h3&gt;

&lt;p&gt;Any long secret you choose. You type it into the webhook settings in the RevenueCat dashboard, RevenueCat sends it unchanged on every request, and the receiving server refuses any request that does not carry it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Which RevenueCat webhook events matter for revenue attribution?
&lt;/h3&gt;

&lt;p&gt;INITIAL_PURCHASE, RENEWAL and NON_RENEWING_PURCHASE carry sales, CANCELLATION carries refunds when its price is negative, and REFUND_REVERSED restores an App Store refund. TRANSFER matters too, because it moves purchases from one App User ID to another.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does RevenueCat do Apple Search Ads attribution by itself?
&lt;/h3&gt;

&lt;p&gt;Yes, for Apple Search Ads only. After the SDK collects Apple's AdServices token, RevenueCat fetches the campaign, ad group and keyword from Apple. Other installs are grouped as Organic in that integration.&lt;/p&gt;

&lt;h3&gt;
  
  
  Is a RevenueCat webhook signature required?
&lt;/h3&gt;

&lt;p&gt;No. The Authorization header is RevenueCat's recommended check, and HMAC signing is an optional, stronger one. With signing on, each request carries a timestamp and an HMAC-SHA256 of that timestamp and the raw body.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sources
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;a href="https://www.revenuecat.com/docs/integrations/webhooks" rel="noopener noreferrer"&gt;Webhooks&lt;/a&gt;, RevenueCat Documentation · read Oct 28, 2026&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.revenuecat.com/docs/integrations/webhooks/event-types-and-fields" rel="noopener noreferrer"&gt;Webhook event types and fields&lt;/a&gt;, RevenueCat Documentation · read Oct 28, 2026&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.revenuecat.com/docs/customers/identifying-customers" rel="noopener noreferrer"&gt;Identifying customers&lt;/a&gt;, RevenueCat Documentation · read Oct 28, 2026&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.revenuecat.com/docs/integrations/attribution" rel="noopener noreferrer"&gt;Attribution integrations&lt;/a&gt;, RevenueCat Documentation · read Oct 28, 2026&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.revenuecat.com/docs/integrations/attribution/apple-search-ads" rel="noopener noreferrer"&gt;Apple Search Ads&lt;/a&gt;, RevenueCat Documentation · read Oct 28, 2026&lt;/li&gt;
&lt;/ol&gt;




&lt;p&gt;&lt;em&gt;Originally published on the &lt;a href="https://attrikit.io/en/blog/revenuecat-attribution" rel="noopener noreferrer"&gt;AttriKit blog&lt;/a&gt;. AttriKit is web-to-app attribution for iOS and Android, built by Clashware Sàrl in Lausanne.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ios</category>
      <category>android</category>
      <category>revenuecat</category>
      <category>mobile</category>
    </item>
    <item>
      <title>Claude Code marketing analytics with an app-scoped key</title>
      <dc:creator>Kamyar Taher</dc:creator>
      <pubDate>Mon, 02 Nov 2026 13:31:20 +0000</pubDate>
      <link>https://dev.to/attrikit/claude-code-marketing-analytics-with-an-app-scoped-key-23dh</link>
      <guid>https://dev.to/attrikit/claude-code-marketing-analytics-with-an-app-scoped-key-23dh</guid>
      <description>&lt;p&gt;&lt;em&gt;This article was drafted with AI assistance and checked against the sources listed.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Key takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Use an app-scoped management key with read permission to review one app's campaigns without granting write access.&lt;/li&gt;
&lt;li&gt;Check the key with whoami, then request campaign reports and decisions for the same explicit reporting period.&lt;/li&gt;
&lt;li&gt;Keep verified, matched, estimated, and organic or unknown results separate when explaining campaign performance.&lt;/li&gt;
&lt;li&gt;Report the decision tool's call unchanged, including insufficient data, and review missing spend or revenue before acting.&lt;/li&gt;
&lt;li&gt;An app key cannot reach sibling apps, workspace ad integrations, or workspace usage controls.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Claude Code marketing analytics means asking Claude Code to read campaign data through connected tools and explain what supports a budget decision. For AttriKit, the connection is its Model Context Protocol server, with access bounded by a management key. Start with an app-scoped key that has read permission, then ask which campaigns need attention and why.&lt;/p&gt;

&lt;h2&gt;
  
  
  Connect the client and check the key
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://modelcontextprotocol.io/docs/2026-07-28/getting-started/intro" rel="noopener noreferrer"&gt;Model Context Protocol&lt;/a&gt;, or MCP, is an open-source standard for connecting AI applications to external systems. &lt;a href="https://code.claude.com/docs/en/mcp" rel="noopener noreferrer"&gt;Anthropic's MCP documentation&lt;/a&gt; describes how Claude Code connects to tools, databases, and APIs through MCP servers.&lt;/p&gt;

&lt;p&gt;AttriKit exposes 39 management tools through its MCP server. These include campaign reports, attribution totals, spend, recent events, and campaign decisions. The server also exposes write tools, so choosing the key is part of setting up the workflow.&lt;/p&gt;

&lt;p&gt;Create the first management key in the AttriKit dashboard under Settings, API keys &amp;amp; MCP. Select the app you want to review and give the key read permission. Keep write permission off for this reporting session.&lt;/p&gt;

&lt;p&gt;Follow &lt;a href="https://attrikit.io/en/docs/mcp" rel="noopener noreferrer"&gt;the MCP setup guide&lt;/a&gt; for the client configuration. AttriKit's Claude Code configuration goes in the project's &lt;code&gt;.mcp.json&lt;/code&gt;. It starts &lt;code&gt;@attrkit/mcp&lt;/code&gt; through &lt;code&gt;npx&lt;/code&gt; and forwards &lt;code&gt;ATTRKIT_API_KEY&lt;/code&gt; from the environment. The macOS login command provides another route: it checks the key and stores it in the Keychain.&lt;/p&gt;

&lt;p&gt;Keep the secret out of the conversation and configuration file. Anthropic documents environment variable expansion in &lt;code&gt;.mcp.json&lt;/code&gt;, so the configuration can reference a variable without containing its value.&lt;/p&gt;

&lt;p&gt;Run &lt;code&gt;claude mcp list&lt;/code&gt; to inspect the client connection. Then ask the agent to call &lt;code&gt;whoami&lt;/code&gt;. AttriKit returns the authenticated workspace, app restriction, and read/write scopes. Check those before requesting campaign data.&lt;/p&gt;

&lt;p&gt;A connected process is not proof of a valid key. AttriKit's troubleshooting guide specifically recommends &lt;code&gt;whoami&lt;/code&gt; when the client connects but tool calls fail authentication.&lt;/p&gt;

&lt;h2&gt;
  
  
  What an app-scoped key cannot reach
&lt;/h2&gt;

&lt;p&gt;App scope and read permission answer different questions. App scope determines which app the key can reach. Read permission determines whether the session can inspect data without changing it.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://attrikit.io/en/docs/management-api#capabilities" rel="noopener noreferrer"&gt;Management API capability matrix&lt;/a&gt; applies to MCP too. An app key stays clamped to its app even when it has write permission.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Resource or operation&lt;/th&gt;
&lt;th&gt;App-key boundary&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Campaign reports, decisions, events, links, and spend&lt;/td&gt;
&lt;td&gt;Its app only&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Other apps in the workspace&lt;/td&gt;
&lt;td&gt;Outside its scope&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Creating another app&lt;/td&gt;
&lt;td&gt;Not allowed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ad integrations&lt;/td&gt;
&lt;td&gt;Workspace-only operations; not allowed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Workspace usage cap and alert email&lt;/td&gt;
&lt;td&gt;Not allowed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Billing, login, membership, workspace deletion, and internal administration&lt;/td&gt;
&lt;td&gt;Not exposed through MCP&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;This matters when a reporting task encounters missing data. If the agent cannot inspect a workspace integration, do not replace the key with a workspace key just to complete the answer. Have it name the unavailable check. Review that configuration separately.&lt;/p&gt;

&lt;p&gt;An app-scoped key with write permission can still change resources belonging to its app. The documented capabilities include updating or archiving that app, rotating its publishable key, and managing keys clamped to it. App scope alone does not make a session read-only.&lt;/p&gt;

&lt;p&gt;For a solo developer reviewing one product, start with the narrower combination: one app and read permission. Expand access only for a separate task that needs a specific write.&lt;/p&gt;

&lt;h2&gt;
  
  
  Ask for a campaign review with a defined period
&lt;/h2&gt;

&lt;p&gt;"Which campaign should I cut?" leaves the reporting period and decision basis unspecified. Give the agent both before it starts.&lt;/p&gt;

&lt;p&gt;Use a prompt like this:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Review only the app allowed by this key. Call whoami first and report the app restriction and permissions without showing the key. Ask me to choose a reporting period, then use that same period for campaign reports, the attribution rollup, and campaign decisions. Page the campaign reports through every next_cursor. In get_campaign_decision, keep the current calls in suggestions apart from the recorded history in data, and name any campaign that has no current call. Report each decision call exactly as returned. Explain the supporting spend, revenue, evidence labels, and missing data. Do not call write tools.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The period instruction prevents a specific mismatch. AttriKit's tool definitions document different defaults for &lt;code&gt;list_campaigns&lt;/code&gt; and &lt;code&gt;get_campaign_decision&lt;/code&gt;. An explicit shared period avoids comparing one report with a decision covering another window.&lt;/p&gt;

&lt;p&gt;Use &lt;code&gt;list_campaigns&lt;/code&gt; for the campaign report and &lt;code&gt;get_campaign_decision&lt;/code&gt; for the engine's scale, cut, or hold call. The decision tool instructs the agent to report that call verbatim. When it returns &lt;code&gt;not_enough_data&lt;/code&gt;, the answer should say that.&lt;/p&gt;

&lt;p&gt;Ask the agent to distinguish the returned decision from its own explanation. A useful answer says which tool supplied the call, which period it covered, and which missing inputs limit the interpretation.&lt;/p&gt;

&lt;p&gt;Do not accept a complete-looking ranking from an incomplete list. The reports support pagination. An agent should follow &lt;code&gt;next_cursor&lt;/code&gt; until there are no more pages before claiming it reviewed every campaign.&lt;/p&gt;

&lt;p&gt;The decision tool pages differently. Its &lt;code&gt;next_cursor&lt;/code&gt; pages the recorded decision history in &lt;code&gt;data&lt;/code&gt;, while the current calls arrive in &lt;code&gt;suggestions&lt;/code&gt;, which are capped at the request's limit and do not page. Following the cursor therefore does not bring in more current calls. Ask the agent to name any campaign that has no current call instead of assuming one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Read the evidence labels before accepting the explanation
&lt;/h2&gt;

&lt;p&gt;AttriKit's product copy distinguishes verified, matched, estimated, and organic or unknown results. Preserve those labels in the answer. A sentence that replaces them all with "attributed installs" removes information you need for the decision.&lt;/p&gt;

&lt;p&gt;The attribution rollup returns evidence-class totals, revenue, marketing efficiency ratio, and data freshness. Ask the agent to show the returned categories separately and explain any field it cannot interpret from the response.&lt;/p&gt;

&lt;p&gt;Estimated campaign totals should remain estimates. AttriKit's reporting documentation also distinguishes campaign-level modeled remainders from results allocated to individual ads. If a remainder has no ad allocation, the agent should leave it at campaign level.&lt;/p&gt;

&lt;p&gt;Organic or unknown should stay separate too. AttriKit's documentation says the organic residual is never reassigned to a campaign. Do not ask the agent to distribute it across campaigns to make the report look complete.&lt;/p&gt;

&lt;p&gt;A useful follow-up prompt is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Restate the recommendation without merging evidence categories. Identify any estimated amount and any campaign-level remainder that is not allocated to an ad. Leave unknown values unknown. Show which returned fields support each conclusion.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Ranges need the same care. The campaign tool exposes &lt;code&gt;aggregation_basis&lt;/code&gt; and tells the agent to read it before describing a lower endpoint. A sum of component endpoints is a scenario, not a guaranteed floor.&lt;/p&gt;

&lt;p&gt;You do not need to interpret those fields from memory. Ask the agent to include the returned basis beside its explanation. Reject wording that turns a scenario into a promise about the campaign's worst possible result.&lt;/p&gt;

&lt;h2&gt;
  
  
  Check missing spend and revenue before cutting
&lt;/h2&gt;

&lt;p&gt;A campaign review should include data completeness, not just a ranking.&lt;/p&gt;

&lt;p&gt;AttriKit's &lt;code&gt;get_spend&lt;/code&gt; tool returns imported spend, cost models, completeness, and spend freshness. Ask for those checks before accepting a recommendation based on return after costs.&lt;/p&gt;

&lt;p&gt;Reading spend and refreshing spend are different operations. &lt;code&gt;get_spend&lt;/code&gt; reads the stored data. &lt;code&gt;pull_network_spend&lt;/code&gt; fetches and imports data, and its tool definition marks it as a write. A reporting session should report stale spend rather than silently refreshing it.&lt;/p&gt;

&lt;p&gt;Revenue needs an explicit basis too. Ask the agent to identify the period, currency information, and missing revenue fields in the report. AttriKit documents that campaign and ad report revenue can be null when it spans multiple currencies. Null is not zero.&lt;/p&gt;

&lt;p&gt;If the question is whether subscription revenue has joined the correct installs, use &lt;a href="https://attrikit.io/en/blog/revenuecat-attribution" rel="noopener noreferrer"&gt;the RevenueCat attribution guide&lt;/a&gt; as the separate diagnostic reference. Do not turn an unresolved revenue question into a confident campaign cut.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Before accepting the campaign review&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The key names the intended app and has read permission without write permission.&lt;/li&gt;
&lt;li&gt;Every report and decision uses the same explicit period.&lt;/li&gt;
&lt;li&gt;Every paginated list has been read through its final page.&lt;/li&gt;
&lt;li&gt;The answer preserves the returned decision call and evidence categories.&lt;/li&gt;
&lt;li&gt;Spend completeness, freshness, and missing revenue are stated.&lt;/li&gt;
&lt;li&gt;The answer separates the proposed budget action from any recorded or executed change.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Keep write tools separate from the analysis
&lt;/h2&gt;

&lt;p&gt;AttriKit's MCP tools include mutations as well as reports. Examples include importing spend, changing a campaign link's status, and recording a campaign decision. The Management API requires write scope for mutations.&lt;/p&gt;

&lt;p&gt;The tool definitions label read tools with &lt;code&gt;readOnlyHint: true&lt;/code&gt; and write tools with &lt;code&gt;readOnlyHint: false&lt;/code&gt;. Those labels describe the tools. Use the key's permissions to restrict this reporting session.&lt;/p&gt;

&lt;p&gt;Do not assume every write has a confirmation screen or preview. The &lt;code&gt;record_campaign_decision&lt;/code&gt; schema has no confirmation or dry-run argument. It takes the app, campaign, decision call, reporting period, and an optional idempotency key, then submits the write.&lt;/p&gt;

&lt;p&gt;For AI agent campaign analytics, keep the first session focused on the answer. If you later want a decision recorded, authorize that as a separate action with an appropriate key.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;record_campaign_decision&lt;/code&gt; freezes the reviewed period's current campaign range, spend, costs, revenue basis, and model version server-side. Its description says callers cannot author that receipt provenance. Record only a returned scale, cut or hold call, with the same period you reviewed. A &lt;code&gt;not_enough_data&lt;/code&gt; call is reported, not recorded: the tool accepts only scale, cut and hold.&lt;/p&gt;

&lt;p&gt;Recording a decision is not evidence that an advertising budget changed. Likewise, &lt;code&gt;update_link_status&lt;/code&gt; changes an AttriKit tracking link's status; do not describe that result as pausing an advertising campaign.&lt;/p&gt;

&lt;p&gt;If a write times out, follow AttriKit's documented retry rule: reuse the same idempotency key for the same request. A timeout can leave the result uncertain. Starting a new request does not resolve what happened to the first one.&lt;/p&gt;

&lt;h2&gt;
  
  
  End with a decision you can inspect
&lt;/h2&gt;

&lt;p&gt;The output of a Claude MCP attribution review should be short enough to check against its inputs. Ask for the app, reporting period, returned campaign call, supporting figures, evidence categories, and unresolved checks.&lt;/p&gt;

&lt;p&gt;A campaign may be a candidate to cut. It may also need a spend correction, a revenue investigation, or more data. The answer should preserve that distinction.&lt;/p&gt;

&lt;p&gt;Finish with a concrete next action for each campaign. Read-only analysis ends with a recommendation and its evidence. Any recording or operational change gets its own authorization and result.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sources
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;a href="https://modelcontextprotocol.io/docs/2026-07-28/getting-started/intro" rel="noopener noreferrer"&gt;What is the Model Context Protocol (MCP)?&lt;/a&gt;, Model Context Protocol · read Oct 30, 2026&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://code.claude.com/docs/en/mcp" rel="noopener noreferrer"&gt;Connect Claude Code to tools via MCP&lt;/a&gt;, Anthropic · read Oct 30, 2026&lt;/li&gt;
&lt;/ol&gt;




&lt;p&gt;&lt;em&gt;Originally published on the &lt;a href="https://attrikit.io/en/blog/claude-code-marketing-analytics" rel="noopener noreferrer"&gt;AttriKit blog&lt;/a&gt;. AttriKit is web-to-app attribution for iOS and Android, built by Clashware Sàrl in Lausanne.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>mcp</category>
      <category>claude</category>
      <category>analytics</category>
    </item>
  </channel>
</rss>
