RevenueCat Affiliate Tracking: The Complete Setup Guide
RevenueCat affiliate tracking means attaching a creator identifier to a subscriber before they buy, then joining it to the RevenueCat webhook events that report the purchase, the renewals and the refund. RevenueCat ships no affiliate feature, so you supply the identifier, the join and the commission ledger. This guide is for a founder whose app runs on RevenueCat and who pays creators without knowing which creator earned what.
What follows is a design you can implement in a week.
#What is RevenueCat affiliate tracking?
RevenueCat affiliate tracking is the practice of carrying a creator identifier from a link click or a code into a RevenueCat customer record, usually as a subscriber attribute, so that every webhook about that subscriber can be joined to the creator who produced them.
Three systems have to agree on one string. Your link service knows which creator owns the click. Your app knows a user id. RevenueCat knows an app_user_id, a product and a price. Nothing joins them automatically, and a wrong join is unrecoverable: RevenueCat will not retroactively say where a subscriber came from.
Two primary sources define the surface: RevenueCat's webhook reference and the customer attributes documentation, which allows "up to 50 unique custom attributes per subscriber, with key names up to 40 characters long and values up to 500 characters long" (verified 12 September 2026). One attribute is all this needs.
We have onboarded apps that ran creator programs for a year on RevenueCat exports and a spreadsheet. Those exports carry revenue and product ids and no creator column, so the year is gone. Set the attribute on day one of any creator experiment. The attribute is free. The missing history is not.
#Does RevenueCat have a built-in affiliate program?
No. RevenueCat has no affiliate, creator commission or partner payout feature as of 12 September 2026. It sells subscription infrastructure: entitlements, paywalls, charts and webhooks. Affiliate attribution sits a layer above, and RevenueCat's answer has been to point developers at a third party.
Four threads on RevenueCat's own community, spanning four years, ask variations of the same thing:
| Thread | Posted | Outcome |
|---|---|---|
| [Cross-platform offer code affiliate program](https://community.revenuecat.com/general-questions-7/best-way-to-introduce-a-cross-platform-offer-code-affiliate-program-1410) | 22 March 2022 | Community workarounds and Branch |
| [Referral program with influencer tracking](https://community.revenuecat.com/dashboard-tools-52/how-to-implement-a-referral-program-with-influencer-tracking-and-generate-monthly-sales-reports-4929) | 9 August 2024 | Zero replies |
| [Built-in affiliate platform](https://community.revenuecat.com/third-party-integrations-53/built-in-affiliate-platform-6227) | 16 April 2025 | Staff: not a feature, use a technology partner |
| [Tracking redemptions for custom codes](https://community.revenuecat.com/dashboard-tools-52/can-we-track-redemptions-for-specific-custom-codes-in-reports-or-the-revenuecat-dashboard-7427) | 10 February 2026 | Staff acknowledge the difficulty |
On that last thread a RevenueCat support engineer wrote that they "have heard of many developers having challenges attributing In-App Purchases with native App Store Promo/Offer Code redemptions" (verified 12 September 2026). Candid, from the vendor closest to the data.
RevenueCat's growth content splits the same way. Their referral program guide, published October 2022 and updated June 2024, covers users inviting users, where nothing is owed to a third party. Their influencer campaign post from October 2023 sends you to Branch, which "allows you to easily manage your links and track their performance combined with RevenueCat analytics."
We think the boundary is right, and not merely convenient for them. Entitlement state has to be correct in milliseconds on a paywall. A creator ledger has to be correct in dollars across refunds and payout rails weeks later, and a product that does both usually does the second badly. What annoys us is that four years of threads still leave founders assembling the answer from forum replies.
#The four ways to attribute a subscription to a creator
Four mechanisms tie a RevenueCat subscription to a creator: store offer codes redeemed in the App Store or Google Play, manual creator codes typed inside your app, deep links captured by an SDK on first open, and web checkout where the creator id rides into billing metadata.
Pick deep links with a manual code as the fallback if you want this to hold; pick store offer codes only when the discount is the campaign.
| Method | Accuracy | What breaks it | Cross-platform | Effort |
|---|---|---|---|---|
| Store offer codes | High, but only on discounted purchases | 6 month expiry, one code per customer per offer | Partly, limits differ per store | Low, then high to administer |
| Manual creator code | High, independent of the install path | Nobody types it, or the paywall is ungated | Yes, everywhere | Low |
| Deep link plus SDK | Highest when click and first open share a device | Reinstalls, a purchase that beats the claim | Universal Links, Install Referrer | Medium to high |
| Web checkout capture | Highest: the browser session is continuous | Redirects that strip the query string | Web purchases only | Low |
Read the store limits from the primary sources. App Store Connect allows 10 active offers per subscription and 1 million codes per app per quarter, custom codes capped at 25,000 redemptions with a six month expiry (verified 12 September 2026). Google Play caps subscription promo codes at 10,000 per quarter per product and grants a free trial of 3 to 90 days rather than a discount (verified 12 September 2026). The 2022 thread hit that asymmetry: "As Android you can only offer free trials not a percentage off a product."
One widely repeated claim needs correcting, ours included. RevenueCat's event reference documents an offer_code field, the "Offer or promotion code used for the transaction," for App Store and Google Play (verified 12 September 2026). Per-code joins through webhooks work today. Only dashboard reporting per code is missing.
Manual creator codes are the most underrated of the four and we default to them for a first launch. They survive a user who sees a video on a phone, installs three days later on an iPad, and subscribes the following week. No device graph survives that. The cost is a conversion tax, since everyone who ignores the field goes unattributed. We take a durable minority over a fragile majority, because a wrong attribution pays the wrong creator.
#How the affiliate id actually reaches the webhook
The affiliate id reaches a RevenueCat webhook by being written into the customer record before the transaction happens. RevenueCat copies subscriber attributes into the event payload it sends you, so the id has to exist on that customer, under a user id your backend also knows, at the moment the purchase clears. Ten steps, in order.
- Log the click. The branded link hits your link service, which records the affiliate and a timestamp, then redirects.
- Issue a claim token. A short-lived token lets the app claim that click later without fingerprinting.
- Let the install happen. Nothing you control runs here, and on iOS this gap kills most attribution schemes.
- Claim on first open.
MyAppAffiliate.start(apiKey:)claims a pending click by itself. CallMyAppAffiliate.attribute(url:)for a Universal Link that opened the app, orMyAppAffiliate.applyCode("LUMI")for a typed code. - Store it twice. Locally for offline reads, and server side so a reinstall does not erase it.
- Bind your user id. Call
MyAppAffiliate.identify(userId:)with exactly the id you pass toPurchases.shared.logIn(...). This is the join key, and the only step that cannot be automated. - Optionally mirror the id into RevenueCat. Read
MyAppAffiliate.attributedAffiliateId()and write it asaffiliate_idthroughPurchases.shared.attribution.setAttributes. Our commission join never reads it, so this buys you a cross-check and RevenueCat-side reporting, nothing more. - Let the purchase run. RevenueCat syncs attributes when the app is foregrounded, backgrounded or making a purchase, per the customer attributes documentation (verified 12 September 2026).
- Receive the webhook. The event carries
app_user_id,subscriber_attributes,product_id,priceandperiod_type. RevenueCat says most are "usually delivered within 5 to 60 seconds of the event occurring" (verified 12 September 2026). - Join and price it. Match
app_user_idto your stored attribution, apply the rate, write a ledger row.
The id lands in two independent places, on your own server at step 5 and inside RevenueCat at step 7. That redundancy looks wasteful until the first time the Keychain value is present while the RevenueCat attribute is missing. We join on app_user_id against our own record and treat the subscriber attribute as a cross-check, never as the source of truth. Step 6 is where we hold the strongest view, having debugged it twice: never let the app invent its own id for RevenueCat.
#Step by step: wiring it up
Wiring RevenueCat affiliate tracking into an iOS app takes two required SDK calls and one webhook endpoint. Start the SDK at launch, bind the user id, then point the webhook at a receiver that verifies and joins. The three steps after that are optional, and each one buys a specific thing.
- Start it once at launch. This runs before any attribution call, in your app delegate or the SwiftUI
Appinit. There is no API URL to pass: the SDK compiles the host in.
import MyAppAffiliate
MyAppAffiliate.start(apiKey: "pk_live_…") // your app's SDK key
// SwiftUI, one line: this also forwards incoming Universal Links.
ContentView().myAppAffiliate(apiKey: "pk_live_…")The SDK now has a device id in the Keychain, retries anything a previous offline launch failed to send, and asks for a deferred match on a fresh install.
- Capture the Universal Link. The SwiftUI modifier above already does this. In UIKit, attach the handler at the root so a cold launch from a creator link is not dropped.
// SwiftUI
.onOpenURL { url in MyAppAffiliate.attribute(url: url) }Associated Domains must be enabled for the link to open the app at all.
- Capture the manual code. Give the user one field, usually on onboarding, and pass the raw string through.
MyAppAffiliate.applyCode("LUMI")This works on a device that never clicked anything, which is why we ship it alongside links.
- Bind the user id. Call this with the same identifier you hand to RevenueCat, not a device id and not an email hash.
MyAppAffiliate.identify(userId: yourUserId)Your backend can now resolve an app_user_id to a stored attribution without guessing.
- Optionally copy the affiliate id into RevenueCat. Store the affiliate id as a subscriber attribute before the paywall can complete a purchase. Our commission join runs on
app_user_idfrom step 4 whether or not you do this, so treat it as reporting and a cross-check.
import RevenueCat
if let affiliateId = MyAppAffiliate.attributedAffiliateId() {
Purchases.shared.attribution.setAttributes(["affiliate_id": affiliateId])
}RevenueCat now carries the value on the customer and echoes it back in every event for that subscriber.
- Point the webhook at your receiver. In the RevenueCat dashboard under Integrations then Webhooks, set the URL and
Authorizationheader, per our webhook setup guide.
The whole integration is about forty lines. Almost every failure we have debugged was an ordering problem, not a code problem, which is why that callout runs longer than most steps.
#Which webhook events move money
Only three RevenueCat event types create a positive commission: INITIAL_PURCHASE outside a trial, RENEWAL, and NON_RENEWING_PURCHASE. An INITIAL_PURCHASE carrying period_type: TRIAL is worth zero and belongs in the funnel, not the ledger. Refunds and transfers subtract or reassign. Everything else, including BILLING_ISSUE, should leave balances untouched.
Map every event to one of three outcomes, credit, debit or ignore, and refuse to let a fourth category exist.
| RevenueCat event | Our normalized type | Effect on the ledger |
|---|---|---|
| `INITIAL_PURCHASE` | `purchase` | Credit at the snapshotted rate |
| `INITIAL_PURCHASE` with `period_type: TRIAL` | `trial_start` | Zero. Counts in the funnel |
| `RENEWAL` | `renewal` | Credit, subject to the renewal cap |
| `NON_RENEWING_PURCHASE` | `purchase` | Credit |
| `CANCELLATION` with `cancel_reason: CUSTOMER_SUPPORT` | `refund` | Debit against the original commission |
| `CANCELLATION`, any other `cancel_reason` | `cancel` | No financial effect |
| `EXPIRATION` | `expiration` | No financial effect |
| `REFUND_REVERSED` | `purchase` | Re-credit, App Store only |
| `BILLING_ISSUE`, `PRODUCT_CHANGE`, others | ignored | None |
Event names verified against RevenueCat's reference on 12 September 2026.
Two rows in that table trip up almost every implementation, ours included.
There is no REFUND event type. RevenueCat's event reference lists nineteen types and REFUND is not among them. A refund arrives as CANCELLATION, which the reference describes as "A subscription or non-renewing purchase was canceled or refunded," and only cancel_reason tells you which of those two happened. Key on the event name alone and refunds land in the same bucket as ordinary churn, so nothing is ever clawed back. If your mapping table has a REFUND row, it is dead code.
Ours had one until we wrote this post and went to check. Fixing it turned up the same shape of mistake in two more of our billing integrations, so it is worth grepping your own.
The trial branch is the other one. A trial start is a payload detail rather than a separate event: it arrives as INITIAL_PURCHASE and only period_type distinguishes it. Key on event type alone and you pay commission on every free trial.
Three fields answer most of what the ledger asks. period_type separates a trial from a paid period. is_trial_conversion, present only on RENEWAL, marks a trial becoming money. price_in_purchased_currency is the amount actually charged, which is why our normalizer prefers it over price.
Our unpopular opinion concerns CANCELLATION: it should change nothing. Founders ask us to freeze a creator's balance the moment a subscriber cancels, because it feels like revenue leaving. It is not. The period was paid and the money landed, so reversing punishes creators for churn they cannot control. For the field-by-field treatment, see how each webhook event changes a commission balance.
#The five things that break attribution in production
Five failure modes account for nearly every dropped commission we have investigated: a purchase made while the customer is anonymous, an alias merge that rewrites the id you joined on, a subscription transferred between App Store accounts, sandbox traffic in the live ledger, and a reinstall that wipes the attribution. All five are identity problems, not billing.
- The anonymous purchase. RevenueCat generates anonymous App User IDs prefixed
$RCAnonymousID:when you have not calledlogIn, per the identifying customers documentation (verified 12 September 2026). If the paywall converts before authentication,INITIAL_PURCHASEarrives with an id your backend has never seen and noaffiliate_id. - The alias merge. When an anonymous customer logs in, RevenueCat merges the identities, and their documentation states that afterwards "there will be only one App User ID within the
original_app_user_idfield," with the rest in analiasesarray. Resolve against all three or you will miss events. - The transfer. RevenueCat's default restore behavior is "Transfer to new App User ID," per their restore behavior page (verified 12 September 2026), emitting a
TRANSFERevent withtransferred_fromandtransferred_to. We stop future accrual and leave matured commissions alone. - Sandbox pollution. Every event carries an
environmentfield valuedSANDBOXorPRODUCTION, and RevenueCat notes it "is only determined by the type of transaction received from the store." TestFlight and StoreKit 2 sandbox purchases are both flagged this way, so a receiver that ignores the field credits creators for your QA sessions. - The reinstall. Anonymous ids are cached locally and cleared on reinstall. If the only copy of the affiliate id lived on the device, it is gone, which is why step 5 above writes it server side too.
The first one hits new integrations within days. In the setups we have reviewed, identify ordering was wrong more often than every other failure combined, and it stays invisible in testing because a developer account is always logged in. You find it when a creator asks why their dashboard shows 40 installs and 2 sales while your revenue chart shows 11 new subscribers. So, an opinion that costs conversions: gate the paywall behind an account, because no webhook cleverness recovers an id that was never written.
#The commission decisions subscriptions force on you
Subscriptions force four decisions a one-time purchase never raises: whether a free trial earns anything, how long renewals keep earning, what a refund does to a commission already credited, and whether the rate applies to the price paid or to what the store deposits. None are technical, and all belong in writing before the first creator signs up.
Follow one subscriber. A $9.99 per month plan, a 20 percent commission recurring for 12 months, a 30 day attribution window and a 30 day hold. A 7 day free trial starts 1 March, converts on 8 March, renews on 8 April and 8 May, then is refunded on 20 May.
The same schedule pays $4.00 on gross and $2.80 on revenue net of Apple's standard cut, which is the entire argument in one line.
| Date | RevenueCat event | Commission on gross | Commission net of a 30% store cut |
|---|---|---|---|
| 1 Mar | `INITIAL_PURCHASE`, `period_type: TRIAL` | $0.00 | $0.00 |
| 8 Mar | `RENEWAL`, `is_trial_conversion: true` | +$2.00 | +$1.40 |
| 8 Apr | `RENEWAL` | +$2.00 | +$1.40 |
| 8 May | `RENEWAL` | +$2.00 | +$1.40 |
| 20 May | Refund of the 8 May period | -$2.00 | -$1.40 |
| **Payable** | **$4.00** | **$2.80** |
The arithmetic in cents, since our formula works in integers: 999 × 2000 / 10000 = 199.8, rounded to 200 cents per period. Three paid periods credit 600, the refund debits 200, so $4.00 is payable. Net of a 30 percent cut the base is 999 × 0.70 = 699.3, each period pays 699.3 × 2000 / 10000 = 139.86, rounded to 140. That leaves 420 minus 140, or $2.80.
The 30 day hold earns its keep here. The 8 May commission is still pending when the refund lands on 20 May, so it reverses before maturing and no negative balance reaches a payout. Set the hold to 7 days and the same subscriber leaves you collecting a clawback from a human being.
Apple's rates make the net column unstable as well as smaller. Apple's subscriptions page states that in a subscriber's first year "you receive 70% of the subscription price at each billing cycle, minus applicable taxes," rising to 85% after a year of paid service (verified 12 September 2026). The App Store Small Business Program gives 85% from the start below 1 million USD in prior-year proceeds. Under it the same period pays 999 × 0.85 × 0.20 = 169.83, rounded to 170 cents.
Three different commissions, one unchanged deal. Pay on gross: the number a creator reads off your paywall is the number they get a percentage of, and it does not move when Apple's terms do. If gross feels expensive, lower the rate. Fourteen percent of gross equals 20 percent of a 70 percent net, and defends itself in an email.
Thirty days is our default window because creator traffic converts late, and it governs the first purchase only. Renewals inherit the original attribution, otherwise a 12 month program would expire halfway through its term.
#Testing without shipping to the App Store
You can exercise the entire RevenueCat affiliate tracking loop without an App Store release. RevenueCat sends a TEST event from the dashboard, sandbox and TestFlight purchases produce real webhooks with environment: SANDBOX, and a stored payload replays as often as you like.
Five assertions are worth automating, in this order.
- Assert honest failure. RevenueCat's webhook documentation requires a 200 response and states that "Any other status code will be considered a failure" (verified 12 September 2026). Return 200 only after the raw payload is stored.
- Assert idempotency. Replay one payload twice and confirm exactly one event and one commission exist. RevenueCat retries "up to 5 times with an increasing delay (5, 10, 20, 40, and 80 minutes)" and reuses the same event
id, so a unique constraint is the whole defence. - Assert the rate. Feed a $9.99 payload at 20 percent and check the ledger row is 200 cents, not 199 or 1998. Rounding bugs are silent and cumulative.
- Assert the trial branch. Send an
INITIAL_PURCHASEwithperiod_type: TRIALand confirm the commission is zero while the funnel still counts it. - Assert the reversal. Refund a purchase you already credited and confirm the balance returns to where it started, including inside the hold period.
Our receiver stores the raw payload before parsing, which is why a parser bug has never cost us a financial event. Those payloads double as the test corpus: when a customer reports a missing commission we replay their real event against a fixed parser.
Delivery is at least once, not exactly once: RevenueCat makes a "best effort for 'at least one delivery' of webhooks" (verified 12 September 2026). Build for duplicates from the first commit. Retrofitting idempotency onto a ledger that already double-paid a creator is an apology and a credit note.
Without a device, we rehearse the loop against the live database with one command:
pnpm --filter @maa/api smokeIt walks click, attribution, webhook and commission in sequence, our pre-flight check before a first real purchase.
#Build it yourself or buy it
Building RevenueCat affiliate tracking in house means shipping seven components: a link service, an attribution SDK, an idempotent webhook receiver, an attribution engine, a commission ledger with clawbacks, a payout rail with tax forms, and a creator dashboard. The receiver is the easy part.
Build it if creator revenue is a rounding error and you enjoy this; buy once creators start emailing about their balances, because that is a support surface, not a feature.
| Component | What it takes | Ongoing burden |
|---|---|---|
| Link service and AASA hosting | Redirects, click logging, an `apple-app-site-association` file per domain | Painful on domain changes |
| Attribution SDK | Link capture, code entry, Keychain storage, a claim handshake, twice for Android | Every OS release is a regression risk |
| Webhook receiver | Token check, persist before parse, unique constraint on the event id | Low |
| Attribution engine | Window logic, alias resolution, transfers, sandbox filtering | Grows with RevenueCat |
| Commission ledger | Rate snapshots, pending and matured states, refund reversals, caps | Where money bugs live |
| Payouts | Stripe Connect onboarding, thresholds, tax forms, failed transfers | Monthly, forever |
| Creator dashboard | Auth, per-creator funnel and earnings | Scales with creator count |
Our estimate, from having built this: an engineer who knows the stack gets the receiver, the ledger and a CSV export working in two focused weeks. Payouts and the creator dashboard are the other 80 percent of the calendar. What founders underestimate is not the code. It is reconciling refunds monthly, chasing a creator whose Stripe onboarding stalled, and answering "why is my balance different".
MyAppAffiliate is the layer that performs the join this post describes. It owns the link and code layer, receives the RevenueCat webhook, applies the commission rules including refund clawbacks, and pays the creator. Flat monthly pricing, no percentage of attributed revenue. First-party attribution with no IDFA and no fingerprinting: the iOS SDK stores a generated device id and the attributed affiliate id. RevenueCat and Stripe are native sources, click to payout in one system. Adapty and Superwall ingest is planned, not shipped.
That is the only pitch here, with a caveat. If your program is two friends with promo codes, a spreadsheet is cheaper than anything we sell. We also compared every tool in the category, with the same columns applied to ourselves.
Does RevenueCat have an affiliate program built in?
No. RevenueCat has no affiliate or creator commission feature as of 12 September 2026, and a support engineer answering the April 2025 community request pointed the asker to a third-party partner. RevenueCat gives you the revenue events; the creator ledger and the payouts are yours.
Can I use Apple offer codes as creator codes?
Yes, with limits. App Store Connect custom codes let you name a code per creator, cap redemptions at 25,000, and set an expiry within six months. RevenueCat's webhook payload carries an `offer_code` field for App Store and Google Play, so a per-creator join is possible.
Which RevenueCat webhook event should trigger a commission?
`INITIAL_PURCHASE` and `RENEWAL` carry the money, and `NON_RENEWING_PURCHASE` covers lifetime opens up. An `INITIAL_PURCHASE` with `period_type: TRIAL` is a funnel event worth zero, so check the field rather than the type. `CANCELLATION` and `EXPIRATION` change no balance, and `BILLING_ISSUE` and `PRODUCT_CHANGE` should be ignored by the ledger entirely.
What happens to the commission if the subscriber refunds?
The commission for that billing period is clawed back. In our ledger a refund writes a negative row against the original commission, so a creator credited $2.00 for May carries minus $2.00 into the next payout. Reverse the refunded period only.
Does this work on Android, React Native and Flutter?
The design does. The click to id to subscriber attribute to webhook path is platform independent, and RevenueCat's webhook shape is the same for Play Store purchases. MyAppAffiliate ships a Swift SDK today, with Android Kotlin planned. Android's deferred primitive is the Play Install Referrer.
Do I need an MMP like Branch or AppsFlyer for this?
Not for creator attribution. RevenueCat's own influencer campaign post routes you to Branch for link management, which does work. But an MMP is built for paid ad networks and SKAdNetwork, and none of them keep a commission ledger or pay a creator.
#What to do next
Set the affiliate_id subscriber attribute in your app today, before deciding anything else about creator commissions. It costs one call, it is free within RevenueCat's 50 attribute allowance, and it is the only step here whose absence cannot be fixed retroactively.
Ship it in your next build with a hardcoded test value if you have to, then work through the iOS SDK guide and set it for real. Last month's creator traffic is gone. Next month's is not.