Affiliate

Pay commissions on sales from your website, your mobile app, or both. One app, one referral link.

Website, mobile app, or both

An Affiliate app is defined by what you connect to it, not by what you picked at creation.

There used to be two separate app types. There is now one. You connect a web payment provider (and give us your site URL), a mobile purchase provider (and give us your App Store / Play Store URLs), or both. Everything else - affiliates, commission rates, products, the referral link, the payout schedule - is shared.

Web rail

Stripe, Polar, Paddle, Whop, Shopify, or WooCommerce, plus a website URL. Turns on the ?ref= link, the web SDK, and web checkout attribution.

Mobile rail

RevenueCat, Adapty, Apple in-app purchases, Google Play, or Stripe, plus your store URLs. Turns on store routing, the mobile SDKs, and install matching.

You can add the second rail later. Editing a live app to connect a mobile provider (or a website and a web provider) keeps the same app, the same affiliates, and the same referral links. Nothing has to be recreated.

How it works

End-to-end flow from app creation to payout.

Selling on your website

1

Business owner creates a web affiliate app on Affiliateo.

2

Sets commission rate (percentage or flat) and connects their Stripe account.

3

Adds the Affiliateo tracking SDK to their website.

4

Affiliates join the app and get unique referral links.

5

When someone clicks a referral link and makes a purchase, the affiliate earns commission.

6

On the 1st of the next month, commissions become payable.

7

Business owner clicks "Pay" to transfer commissions to affiliate wallets.

Selling inside your mobile app

1

Business owner creates a mobile affiliate app on Affiliateo.

2

Sets commission rate and connects RevenueCat.

3

Adds the Affiliateo SDK to their mobile app.

4

Affiliates join the app and get unique referral links.

5

When someone installs through the link and makes an in-app purchase, the affiliate earns commission.

6

On the 1st of the next month, commissions become payable.

7

Business owner clicks "Pay" to transfer commissions to affiliate wallets.

Running both? Do both. They are the same app, so an affiliate who drives a website sale and an app install is one affiliate with one balance.

Affiliates get one link. Where it lands is decided when it is clicked.

1

An iPhone or iPad click goes to your App Store URL, when you have set one.

2

An Android click goes to your Play Store URL, with the Play install referrer appended.

3

Everything else (desktop, tablets we can't place, anything unusual) goes to your website, with ?ref= appended.

4

Mobile-only app with no web rail? The click falls through to whichever store URL exists, so a desktop visitor still lands on a store page they can send to their phone.

5

Selling on both, and want phones on your website anyway? Turn on Send phones to my website in the Mobile app section when you create or edit the app. Every affiliate link then opens your website on phones too, and an app install that follows is still matched to the click. It only works while your website's payment provider is connected, and stays greyed out until one is.

Referral links come in three formats: a short link (affiliateo.com/r/CODE), a username link, and a direct link on your own domain (yoursite.com?ref=username). Direct links skip the redirect entirely; the tracking SDK captures the ?ref= parameter when the visitor lands.

How referrals are tracked

The two rails do not track the same way. Plan your program around the difference.

Website
Mobile app
MethodDeterministicProbabilistic
What carries the referralThe ?ref= parameter on the link. The SDK reads it on landing and writes it into first-party storage on your own domain.Nothing. The App Store strips every parameter off a link before the install, so the code the affiliate shared does not reach the installed app.
How the sale is matchedThe exact code that was written down is read back at checkout and passed to your payment provider. The affiliate who was clicked is the affiliate who gets paid.On first app open the SDK reports the device to us and we look for a recent click that plausibly came from it: IP address (exact, or the same /24 subnet), operating system, country, and how recent the click was. The best-scoring click wins.
Matching windowNo separate step. The code is read at checkout.7 days from the click. Older clicks are not considered, and recent clicks score higher than old ones.
When it can be wrongEssentially never. It fails only if the visitor clears storage or switches browsers before buying.Shared office or carrier IPs, VPNs, and long gaps between click and install can miss a match. A subnet-only match needs corroborating signals before it counts at all.
Attribution window on the sale60 days, rolling. Reset by every tagged pageview and every conversion.60 days, rolling, counted from the install match.

Install attribution is a best-effort match, not a receipt. Expect a share of installs to go unattributed, and expect your mobile numbers to be a floor rather than an exact count. This is a property of the App Store, not of Affiliateo: no one can pass a parameter through an Apple install, so every attribution vendor is making the same kind of guess.

Android is a partial exception: the Play Store does forward an install referrer, and our redirect appends one. Today Affiliateo reads it to work out which ad network drove the install. Affiliate credit on both stores still comes from the fingerprint match above.

Once a purchase is attributed, both rails behave identically: the rate is locked at conversion time and every renewal of that subscription keeps crediting the same affiliate, however old the original click is.

Commission structure

Flexible commission models for your business.

Type
Example
Description
Percentage20% of each saleAffiliate earns a percentage of the transaction amount
Flat rate$5 per saleAffiliate earns a fixed amount per conversion
Per-product custom$5 on one product, 15% on anotherSet one default rate for the app, then override individual products with their own percentage or flat amount. Products you don't customize fall back to the default rate.

A 1% platform fee applies to each affiliate-referred sale, in addition to the commission you set.

Commission rate is locked in when an affiliate joins. The rate cannot be lowered for existing affiliates.

Commission is calculated on the gross sale price, not the net amount after store fees.

Maximum commission, per rail

The cap depends on who takes a cut before the money reaches you.

Sale goes through
Max commission
Why
Apple in-app purchases, Google Play, and store purchases through RevenueCat or Adapty70%Apple and Google take up to 30% off the top. A higher rate would lose money on every sale.
Stripe, Polar, Paddle, Whop, Shopify, WooCommerce, and website purchases through RevenueCat or Adapty90%No store cut. The remaining 10% covers payment processing and the platform fee.
Flat payouts (any rail)$50Applies to the app-wide flat rate. Per-product flat payouts are set per product and are not capped at $50.

The minimum percentage rate is 1%. Note that a mobile app billed through Stripe rather than the stores sits in the 90% row: nobody takes a store cut, so the store ceiling does not apply.

The same goes for a purchase made on a website that RevenueCat or Adapty reports (RevenueCat's own Web Billing, or a Stripe or Paddle purchase they track). No store took a cut, so that sale is capped at 90%, not 70%. A store purchase through the same provider stays capped at 70%.

Where the cap is applied

1

Per-product rates are capped when you save them. Each product in your rates editor comes from exactly one catalogue, so we know its rail at that moment: a product from your mobile catalogue caps at 70%, a product from your web catalogue caps at 90%, on the same app.

2

The app default rate and per-affiliate deals are capped at the sale. One number covers both rails, so it cannot be judged at config time. You may save 90%; a store-mediated sale is then paid at 70% and everything else at 90%. We never reject the save, we just never pay more than the rail allows.

Apple and Google take up to 30% of each sale. The maximum affiliate commission is 70% to account for this.

Payment providers

Affiliateo connects to six different payment platforms. Each provider has its own setup and install instructions, but all of them feed into the same affiliate tracking + commission logic.

Stripe

OAuth Connect

Best for SaaS and digital products. Click Connect → approve in Stripe popup → done. Our SDK or paste-in script captures the affiliate, you pass it to Stripe metadata at checkout.

Storefront install

npm install @affiliateo/web (or paste t.js)

Polar

OAuth Connect

Open-source Stripe alternative for digital creators. Same OAuth shape as Stripe. single click to connect.

Storefront install

npm install @affiliateo/web (or paste t.js)

Paddle

Paste API key

Merchant-of-Record platform for SaaS. Paste a Paddle API key from your dashboard; we auto-create the webhook subscription on your account.

Storefront install

npm install @affiliateo/web (or paste t.js)

Whop

Paste API key

Creator monetization platform (memberships, communities, courses). Paste a Whop Company API key; we auto-create the webhook.

Storefront install

npm install @affiliateo/web (or paste t.js)

Shopify

Paste store credentials

For Shopify dropshippers. Create a custom app in your Shopify admin, paste shop domain + access token + API secret. We auto-create the webhooks.

Storefront install

Paste shopify-affiliate.js script into theme.liquid

WooCommerce

OAuth Connect

For WordPress-based stores. Type your shop URL → click Connect → approve in your WC admin popup. WooCommerce generates and sends us the keys directly.

Storefront install

Download the Affiliateo WordPress plugin and activate

Pick how you take payments

Four ways to wire purchases into your affiliate pipeline. Pick one (or combine Apple + Google for direct native).

RevenueCat, Adapty, the App Store and Google Play connect beside each other, so an app can move from one to another without a gap, and a purchase two of them report is counted once. Connecting one alongside a web provider is what makes the app dual-rail: it does not replace the web side. Selling on the web as well? See how RevenueCat or Adapty work together with Stripe or Paddle.

SDK integration

Install the SDK for each rail you run. A dual app installs both, using the same app ID on either side.

Website

Add the Affiliateo SDK to your website to track affiliate referrals and attribute purchases.

Applies to Stripe, Polar, Paddle, and Whop. Shopify and WooCommerce use their own install paths. see below.

1. Install

Terminal
npm install @affiliateo/web

2. Add the provider to your app

app/layout.tsx
import { AffiliateoProvider } from '@affiliateo/web';

export default function RootLayout({ children }) {
  return (
    <AffiliateoProvider appId="YOUR_APP_ID">
      {children}
    </AffiliateoProvider>
  );
}

3. Pass attribution to Stripe checkout

Checkout button (client)
'use client'
import { getAttribution } from '@affiliateo/web';

async function handleCheckout() {
  const attribution = getAttribution();
  const res = await fetch('/api/checkout', {
    method: 'POST',
    body: JSON.stringify({ attribution }),
  });
  const { url } = await res.json();
  window.location.href = url;
}
app/api/checkout/route.ts
export async function POST(req) {
  const { attribution } = await req.json();

  const session = await stripe.checkout.sessions.create({
    line_items: [...],
    mode: 'payment',
    metadata: attribution,
  });

  return Response.json({ url: session.url });
}

That's it! The provider tracks sessions and captures affiliate attribution automatically. Stripe metadata links each purchase back to the referring affiliate.

Mobile app

Add the Affiliateo SDK to your mobile app. Choose your platform below.

1. Install

Terminal
npm install @affiliateo/react-native @react-native-async-storage/async-storage @react-native-community/netinfo

2. Wrap your app with the provider

App.tsx
import { AffiliateoProvider } from '@affiliateo/react-native';

export default function App() {
  return (
    <AffiliateoProvider appId="YOUR_APP_ID">
      <YourApp />
    </AffiliateoProvider>
  );
}

Track a screen (one line)

Any screen file
import { useAffiliateoScreen } from '@affiliateo/react-native';

export default function HomeScreen() {
  useAffiliateoScreen('HomeScreen');
  return <YourScreenUI />;
}

Fingerprint matching, RevenueCat attribution, and session tracking are automatic.

Where the affiliate's code goes at checkout

The SDK remembers who sent the visitor. At checkout you hand that to your payment provider, and it hands it back to us with the sale.

There are four values. The first two matter on every provider.

Value
What it is
What it does
affiliateo_refThe affiliate's code, as the SDK stored it.Credits the sale to that affiliate.
affiliateo_visitor_idThe visitor's id, from the SDK.Checks the click is still inside the 60 days, and ties the sale to that visitor.
affiliateo_app_idYour app ID.Stripe only: says which app the sale belongs to.
affiliateo_user_idOptional. Your own id for the buyer, the one you pass to identify().Lets a purchase be credited to a click the same user made on another device (see One customer, two devices).

Per provider

Provider
Where the values go
Good to know
Stripe CheckoutThe Checkout Session's own metadata.Send affiliateo_app_id too: without it the sale is only recorded when one app alone uses that Stripe account. A Payment Link cannot take metadata per visitor, so it carries the code alone, as client_reference_id in its address.
Stripe without CheckoutThe metadata of the PaymentIntent, or of the Subscription, that your server creates.affiliateo_app_id is required. See below.
PaddleThe transaction's custom_data (customData in Paddle.js).A checkout opened with no custom data is recorded as organic.
PolarThe checkout's metadata.A Checkout Link carries the code alone, as reference_id in its address.
WhopThe checkout configuration's metadata.Whop passes it on to the payment, which is where we read it.
ShopifyNothing to add. The theme script writes them into the cart, and Shopify carries them to the order.An affiliate's discount code on the order counts too.
WooCommerceNothing to add. The plugin writes them into the order.An affiliate's coupon code on the order counts too.

A Checkout Session your server creates

app/api/checkout/route.ts
export async function POST(req) {
  const { attribution } = await req.json();

  const session = await stripe.checkout.sessions.create({
    line_items: [...],
    mode: 'payment',
    metadata: attribution,
  });

  return Response.json({ url: session.url });
}

A Payment Link (no server)

Buy button (client)
// A Payment Link takes no metadata per visitor: it carries one value
// in its address. Send the visitor id; the affiliate is read from it.
const visitorId = localStorage.getItem('affiliateo_visitor_id');
const ref = localStorage.getItem('affiliateo_ref');
const url = new URL('https://buy.stripe.com/YOUR_PAYMENT_LINK');
const reference = visitorId || ref;
if (reference) url.searchParams.set('client_reference_id', reference);
window.location.href = url.toString();

A Payment Link carries the code and nothing else. Stripe accepts letters, digits, dashes and underscores there and silently drops any other value, so a code with a dot in it does not arrive. A Checkout Session has no such limit.

Renewals need nothing more, on any provider. Each one is credited to the affiliate of the subscription's first payment.
Discount codes: with Coupon codes turned on for the app, a buyer who types an affiliate's code at a Stripe Checkout, a Shopify checkout or a WooCommerce checkout credits that affiliate, even with no link, and the code wins over a link. Paddle, Polar, Whop and Stripe without Checkout credit by the values above only.
Older spellings still work. On a Stripe Checkout Session the code is also read from ref in the metadata, then from the session's client_reference_id (a value shaped like a uuid is taken for one of your own ids and ignored). On Paddle, Polar, Whop and Shopify it is read from affiliateo_ref, then ref, then client_reference_id, then reference_id.

Stripe without Checkout

Some payments never make a Checkout Session: Stripe's PaymentSheet inside an app, the Payment Element on your own page, or a subscription your server starts through the API. Your server creates the PaymentIntent or the Subscription itself, so the values go in that object's own metadata. The code is in the Stripe without Checkout tab above.

affiliateo_app_id is required. It is how a payment opts in: a PaymentIntent or a Subscription without it is not recorded, with or without a code.
For a subscription, set the values on the Subscription. Stripe does not copy metadata from one object to another, so values on its first payment alone are not read.
Only the affiliateo_ names are read here: affiliateo_app_id, affiliateo_ref, affiliateo_visitor_id and affiliateo_user_id. The older spellings above are not.
Renewals, refunds and disputes then follow automatically, as they do after a Checkout Session.
Discount codes do not credit an affiliate on this path.
A one-time PaymentIntent names no product, so it earns the app's rate, or the affiliate's personal rate. A subscription has a price and a product, so its per-product rate applies.

Track conversion goals

Goal names must be lowercase letters, digits, underscores, hyphens, or colons on both rails, and optional metadata (a second argument) is bounded to 4KB if you want to slice funnels by property later.

Website

Pageviews auto-track. For specific moments that matter to your funnel (signup_completed, trial_started, initiate_checkout, etc.), you have two options depending on your integration: npm SDK users call track() from a React hook (and trackPageview() for modal or virtual-route pageviews). Script-tag users can additionally drop a data-affiliateo-goal attribute on any button, or data-affiliateo-scroll on a section to fire when it scrolls into view (great for landing pages where you want to know which sections convert).

Fire a custom goal from React

app/signup-success/page.tsx
'use client'
import { useEffect } from 'react'
import { useAffiliateRef } from '@affiliateo/web'

export default function SignupSuccess() {
  const { track } = useAffiliateRef()
  useEffect(() => {
    track('signup_completed', { plan: 'free' })
  }, [])
  return <div>Welcome!</div>
}

Track virtual pageviews (modals, SPA routes)

Modal or virtual route open handler
'use client'
import { useEffect } from 'react'
import { useAffiliateRef } from '@affiliateo/web'

export default function PricingModal({ open }: { open: boolean }) {
  const { trackPageview } = useAffiliateRef()
  useEffect(() => {
    // Fire a pageview for the modal so it shows up in your funnel
    // even though no real route change happened.
    if (open) trackPageview('/pricing-modal')
  }, [open])
  return null
}

Mobile app

On native SDKs (React Native, Swift, Kotlin, Flutter) screens are tracked when you call page(name) per screen. This is the Mixpanel / Amplitude model. predictable, no ghost events. For specific moments that matter to your funnel (signup_completed, trial_started, feature_used, etc.), call track(). Vanilla / WebView (Capacitor, Ionic, Cordova) loads the m.js script and pageviews on URL change are auto-fired, same as the web SDK.

Track a custom event

SignupButton.tsx
import { useAffiliateRef } from '@affiliateo/react-native';

function SignupButton({ onNext }) {
  const { track } = useAffiliateRef();
  return (
    <Pressable onPress={() => {
      track('signup_completed');
      onNext();
    }}>
      <Text>Continue</Text>
    </Pressable>
  );
}

Identify signed-in users

Without identify(), the same person browsing on phone and buying on laptop counts as two unrelated visitors and your funnel splits across devices. Call it once after sign-in so Affiliateo can stitch the journey to one person. Idempotent. safe to fire on every page load or app launch. On a dual app this is what ties a website visit and an app install to the same human.

Call logout() from your own sign-out, so whoever uses the phone or browser next is not counted as the person who left. If somebody else signs in on the same device, it simply counts as them from then on. Placeholder ids like "null", "guest" or "0" are ignored.

Stitch this browser to a logged-in user

After sign-in (top-level layout)
'use client'
import { useEffect } from 'react'
import { useAffiliateRef } from '@affiliateo/web'
import { useUser } from './your-auth'

export default function AuthBoundary({ children }) {
  const { identify } = useAffiliateRef()
  const user = useUser()
  useEffect(() => {
    if (user) identify(user.id)
  }, [user])
  return <>{children}</>
}

// In your sign-out handler:
// const { logout } = useAffiliateRef()
// await logout()

One customer, two devices

Identify is also what lets a sale follow a customer from one device to another. Someone taps an affiliate's link on their phone and buys on their computer, or the other way round: the buying device never saw the link. Call identify() with the same id on your website and in your app, and a new purchase with no affiliate on its own device is credited to the latest affiliate click the same user made on another device, inside the usual 60 days.

It only fills sales that would have been organic. An affiliate link the buying device itself came through always wins, and so does a discount code typed at checkout.
It applies to new purchases and trials. Renewals keep following the subscription's first payment.
The id must be hard to guess: at least 8 characters, and not a plain number of 12 digits or fewer. A uuid, or a Firebase or Auth0 id, works. With a shorter or numeric id only same-device attribution applies, as before.

The purchase has to be tied to the user. How depends on where the sale comes from:

The sale comes through
How we know who bought
Stripe, Paddle, Polar, Whop, Shopify, WooCommerceAutomatically, when the checkout carries the affiliateo_visitor_id of a visitor you identified. On Stripe, Paddle, Polar and Whop you can also pass the id yourself, as affiliateo_user_id in the checkout's metadata.
RevenueCatIts App User ID: set it to your user's id when they sign in. The device the app identified counts too.
AdaptyIts customer user id: set it to your user's id when they sign in. The device the app identified counts too.
App Store and Google Play, connected directlyThe device. The purchase names the device that bought, and identify() told us who is signed in on it. The purchase's tag can also be your own account id, when you pass that same id to identify() (App Store, Google Play).

Give affiliates free access

Let an affiliate use your paid app without paying, for as long as you choose, and take it back whenever you want. Works with RevenueCat, the App Store, Google Play, Adapty and Stripe.

1

On your app's Affiliates tab, switch Access on. A small box asks what they get (a plan, or full access), for how long (1 week, 1 month, 3 months, 1 year or forever) and the email they use in your app.

2

It ends by itself when the time is up: nothing renews and nobody is charged. Switch it off to take it back sooner. It is also removed if you ban them, if they leave, or if the app ends.

Apps on RevenueCat

We give it as a promotional entitlement in your own RevenueCat project, so there is no free access code to add. Just make sure your app sends each person's email to RevenueCat when they sign in: that address is how we find them in your project.

Send the email to RevenueCat at sign-in

At sign-in
import Purchases from 'react-native-purchases';

// After Purchases.logIn(user.id), for the person who just signed in:
await Purchases.setEmail(user.email);

Apps on the App Store, Google Play, Adapty or Stripe

Apple and Google can't give anyone a subscription from outside your app, so your app asks us. Add two lines from our SDK: pass the email to identify() when someone signs in, then call freeAccess() after sign-in and every time the app opens. Only a scrambled copy (SHA-256) of the email leaves the phone.

Unlock while free access is active

After sign-in, and on every launch
const { identify, freeAccess } = useAffiliateRef();

// At sign-in. The email is hashed on the phone; only the hash is sent.
await identify(user.id, { email: user.email });

// After sign-in and on every launch: unlock while it says yes, lock again
// when it says no (the length the owner chose has ended).
const access = await freeAccess();
// access.productId: the plan the owner chose, or null for full access
// access.expiresAt: when it ends, or null for no end
setHasFreeAccess(access.active);

Keep the purchase check you already have and add this one to it: unlock your paid features when the person paid (through StoreKit, Google Play Billing, Adapty or Stripe) or when freeAccess() says active. productId is the plan you picked (null means everything) and expiresAt is when it ends (null means no end). When it says not active, lock them again: that is how an ended length, or you switching it off, reaches the phone.

Free access for every affiliate

To skip the switch, turn on Free access for every affiliate in your app's settings and pick a length (and a plan, on apps without RevenueCat). Everyone who becomes an affiliate then gets it, however they join: on Affiliateo, in the Affiliateo app, inside your own app, or by a link. Turning it on also gives it to the affiliates you already have.

Free access it gave follows the setting: a new length moves each person's end (counted from when they got it), a new plan moves their plan, and Off takes it back. If you change one affiliate's free access by hand, they keep their own and the setting leaves them alone.

One call from your server

Most apps need nothing else. Your server already calls our "become an affiliate" endpoint each time someone opens your affiliate screen. Add app_email to that call: the email they are signed in with in your app. Send it every time and it keeps itself up to date.

POST /api/v1/businesses/{slug}/apps/{appId}/affiliates
{
  "email": "jane@example.com",
  "app_email": "jane@icloud.com"
}

If your app has its own rules, also send free_access. It counts when they become an affiliate: false gives this person none, and a length (like forever for your VIPs) gives them that instead of your automatic setting.

A new app_email never takes free access away. Someone who changes their email in your app keeps the free access they already have. To give it to a different person, use the switch on the Affiliates tab.

How we find the affiliate in your app

We only ever match on email: the one your app sends or you type for them, or else their Affiliateo email. Never on who referred whom. That is deliberate: the people attributed to an affiliate are the customers they REFERRED, so treating one of those as the affiliate could hand a year of paid features to a stranger.
Waiting means nobody in your app (or your RevenueCat) has signed in with that email yet. You don't need to do anything: we look again every 10 minutes at first, then less often, and it starts by itself once they sign in.
If two accounts in your app use the same email, we can't tell which one is the affiliate, so neither gets it. Type an email only they use, or remove the extra account.
Guests can't be found. Someone who uses your app without signing in has no email for us to match, so ask them to sign in.
People who signed in with Apple and chose Hide My Email use a relay address in your app (it ends in @privaterelay.appleid.com). The affiliate can find it on their iPhone in Settings > Apple Account > Sign in with Apple. If your app sends app_email, it already sends that address. If not, type it as their email in the box.

On RevenueCat apps with more than one entitlement (for example basic and premium), the box lets you pick which one each affiliate gets. On the other apps it lists your store plans.

From your own server, the free access endpoint does what the switch does, and free_access_auto on the app is the automatic setting. See the API reference

Give affiliates a clean link on your own domain

Add a landing page URL in your app settings and affiliates get a fourth link format: yoursite.com?ref=username, with no redirect through affiliateo.com. Put one of these snippets on that page and every arrival with a ref code is recorded, so installs still match back to the right affiliate. Pick one flavor:

Add to your landing page

HTML
<script defer
  data-app="YOUR_APP_ID"
  data-redirect="store"
  data-ios-url="https://apps.apple.com/us/app/your-app/id123456789"
  data-android-url="https://play.google.com/store/apps/details?id=com.yourapp"
  src="https://affiliateo.com/t.js"
></script>

The page instantly forwards affiliate visitors (?ref= in the URL) to the right store for their device; everyone else sees your page normally. Clean link, less friction.

Fastest: redirect at your server

Bounce affiliate visitors before your page even loads

The snippet above runs in the browser, so visitors briefly see your page before the bounce. If you control your server or edge, you can skip that entirely: 302-redirect any request arriving with ?ref= to https://affiliateo.com/r/your-app-slug/{ref} (your app's slug is in its Affiliateo URL). The visitor's browser follows the redirect, so the click is logged with their real IP and device and installs match exactly like short links. Important: do not report the click server-to-server instead, because it would arrive from your server's IP and install matching would break. With this in place you don't need the landing page snippet at all.

Add to your server

worker.js
export default {
  async fetch(request, env) {
    const url = new URL(request.url)
    const ref = url.searchParams.get('ref')
    if (ref && request.method === 'GET') {
      return Response.redirect(
        `https://affiliateo.com/r/your-app-slug/${encodeURIComponent(ref)}`,
        302,
      )
    }
    return env.ASSETS.fetch(request) // or your existing handler
  },
}

Any host works the same way: if the request has ?ref=, send a 302 to your app's affiliateo.com/r/ link. Organic traffic falls through to your normal page.

Multi-domain attribution

If your property spans more than one domain (marketing site on marketing.com and your app on app.merchant.com), affiliate ref codes die at every domain boundary because browsers sandbox storage by origin. Without help, an affiliate who sends traffic to your marketing site gets no commission when the visitor signs up on your app domain. Add the other domains via data-allowed-hostnames (script tag) or allowedHostnames (npm SDK prop) and the SDK auto-decorates outbound links with the visitor identity. The receiving domain reads those params on landing, strips them from the URL bar, and the funnel sees one continuous visitor. Both sides need the SDK installed and both need to list the OTHER domains (each side names everyone except itself).

Pass allowedHostnames to the provider

app/layout.tsx (multi-domain merchants)
import { AffiliateoProvider } from '@affiliateo/web';

export default function RootLayout({ children }) {
  return (
    <AffiliateoProvider
      appId="YOUR_APP_ID"
      allowedHostnames={['app.merchant.com', 'docs.merchant.com']}
    >
      {children}
    </AffiliateoProvider>
  );
}

Development & debugging

By default the SDK is configured for production safety: localhost is blocked (so npm run dev doesn't pollute your funnel), iframes are blocked (so scrapers and preview tools don't count as pageviews), and debug logs are silent. Turn any of these on with the props or attributes below. Also exposed: a flush() method to force queued events out the door right before a redirect, so they don't get lost when the page navigates away.

Opt-in dev flags (allowLocalhost, allowIframe, debug)

app/layout.tsx (dev / debugging)
import { AffiliateoProvider } from '@affiliateo/web';

export default function RootLayout({ children }) {
  return (
    <AffiliateoProvider
      appId="YOUR_APP_ID"
      // Turn on for end-to-end tests against localhost.
      // Leave false (the default) in production.
      allowLocalhost={process.env.NODE_ENV !== 'production'}
      // Print decision logs to the browser console. Useful when
      // investigating "why aren't my events showing up?".
      debug={process.env.NODE_ENV !== 'production'}
      // Set to true if your site is legitimately embedded inside
      // an iframe (e.g. an embed widget shipped to partners).
      // allowIframe
    >
      {children}
    </AffiliateoProvider>
  );
}

Force flush before a redirect

Checkout button
'use client'
import { useAffiliateRef } from '@affiliateo/web'

export default function CheckoutButton() {
  const { track, flush } = useAffiliateRef()
  return (
    <button onClick={async () => {
      track('initiate_checkout', { plan: 'pro' })
      // Force events out NOW before the redirect kills the page.
      // Default flush timer is every 5s, too slow for a redirect.
      await flush()
      window.location.href = '/api/checkout'
    }}>
      Subscribe
    </button>
  )
}

allowLocalhost matches localhost, 127.0.0.1, RFC1918 IPs, and the reserved .local / .test / .example / .invalid / .localhost / .lvh.me / .localtest.me TLDs. Production-style hosts (.vercel.app, .pages.dev, custom domains) are always allowed.

Shopify storefront install

Drop a single script into your theme. It captures affiliate ref-codes from URL parameters, syncs them into Shopify cart attributes, and tracks visitor pageviews. all in one tag.

Paste before </body> in layout/theme.liquid

layout/theme.liquid
<script defer
  data-app="YOUR_APP_ID"
  src="https://affiliateo.com/shopify-affiliate.js"
></script>

In Shopify admin → Online Store → Themes → Edit code → open layout/theme.liquid → paste before </body> → Save. Works on every Online Store 2.0 theme (Dawn, Crave, Sense, etc.) and on headless / Hydrogen storefronts.

WooCommerce plugin install

Download the Affiliateo WordPress plugin, install it on your store, and paste your app ID. The plugin handles ref-code capture (cookie + checkout hook → order meta_data) and visitor analytics.

1

Download affiliateo-woocommerce.zip from your wizard's install step.

2

In your WordPress admin: Plugins → Add New → Upload Plugin → choose the .zip → Install Now → Activate.

3

Go to WooCommerce → Settings → Advanced → Affiliateo and paste your app ID. Save.

4

Affiliate links work immediately. The plugin captures the ?ref= parameter, persists it through checkout, and writes it to the order's _affiliateo_ref meta. which our webhook handler picks up automatically.

RevenueCat setup

Connect RevenueCat to track in-app purchases.

1

Go to your RevenueCat dashboard → Project Settings → API Keys.

2

Create a new V2 secret key.

3

Paste the key in the app settings on Affiliateo.

4

Affiliateo automatically creates a webhook in your RevenueCat project.

Once connected, purchases are tracked automatically and attributed to the referring affiliate via subscriber attributes.

RevenueCat on your website

A buyer who never opened your app has no install to match. The page hands the affiliate to RevenueCat with the purchase instead.

1

Put the website SDK on the page that sells (see SDK integration). It stores the affiliate's code and the visitor's id.

2

Pass them as metadata to the purchase() call of RevenueCat's Web SDK, as affiliateo_ref and affiliateo_visitor_id. You can add affiliateo_user_id, your own id for the buyer.

Pass the affiliate with the purchase

Buy button (client)
'use client'
import { Purchases } from '@revenuecat/purchases-js';
import { getAttribution } from '@affiliateo/web';

async function handlePurchase(rcPackage) {
  const { affiliateo_ref, affiliateo_visitor_id } = getAttribution();
  await Purchases.getSharedInstance().purchase({
    rcPackage,
    metadata: {
      ...(affiliateo_ref ? { affiliateo_ref } : {}),
      ...(affiliateo_visitor_id ? { affiliateo_visitor_id } : {}),
      // affiliateo_user_id: user.id, // optional: your own id for the buyer
    },
  });
}
The metadata is read on a new purchase only: a subscription's first payment, or a one-time purchase. Renewals follow that first payment.
It is read for purchases made on a website only: RevenueCat's own Web Billing, and Stripe or Paddle purchases RevenueCat reports. App Store and Google Play purchases keep using the subscriber attributes your app sets.
When the customer also carries a code from your app, the one on the purchase wins if it names an active affiliate inside the 60 days: it is the latest click.
A purchase made on a website has no store cut, so it is capped at 90%, not 70% (see Maximum commission, per rail).

A website with no app in a store

Pick RevenueCat when you create the program and enter your website address where the App Store or Google Play link would go. No store link is needed.
Your affiliates' links then open your website, and the website SDK is the one to install. There is no mobile SDK to add.
The program's main rate can go up to 90%. Per-product rates and personal deals stay capped at 70%.
Sell through the purchase() call above. A purchase link hosted by RevenueCat cannot carry the affiliate's code, so a sale through one is recorded as organic.
This is for RevenueCat only. If you sell on the web through Adapty, connect the Stripe or Paddle account behind it instead.

RevenueCat or Adapty together with Stripe or Paddle

A purchase that reaches us twice is counted once.

If you sell on the web through Stripe or Paddle and send those purchases to RevenueCat or Adapty as well, each sale is reported twice. When the same Stripe or Paddle account is also connected to the app directly, that connection is the record of the money, and the sale is counted once.

Selling on the web through Stripe or Paddle? Connect it directly, beside RevenueCat or Adapty. Refunds and renewals are then exact, because they come from the processor that moved the money.

Connected to the app
A website purchase is recorded from
Good to know
RevenueCat and the same Stripe accountStripe. The amount, the product and the dates are Stripe's, and so are the renewals, refunds and disputes.A Checkout Session, or a payment that names your app, is recorded with the values it carries. Any other Stripe purchase is looked up in your Stripe account and recorded with the affiliate RevenueCat's customer carried. Putting the values on the Stripe payment yourself is the surest.
RevenueCat and the same Paddle accountPaddle.The affiliate has to be in the transaction's custom_data. RevenueCat's copy is not read.
RevenueCat aloneRevenueCat's report.RevenueCat's own Web Billing is always recorded from RevenueCat, whatever else is connected.
Adapty and the same Stripe or Paddle accountStripe or Paddle.Adapty's copy is not read, so put the values on the Stripe or Paddle checkout (see where they go).
Adapty aloneAdapty's report.The affiliate is the one your app set on the Adapty profile. A refund of the purchase is not taken back: Adapty's refund event carries no amount. Connect Stripe or Paddle directly if refunds must count.
Adapty and RevenueCatRevenueCat's report only.A website purchase Adapty reports is not recorded while RevenueCat is connected too. Make sure RevenueCat hears of it.

This is about purchases made on a website. App Store and Google Play purchases are not affected: two providers that report the same store purchase already count it once.

A web checkout inside your app

When your app sells through Stripe, Paddle, Polar or Whop rather than the App Store or Google Play, pass the affiliate to the checkout yourself.

1

In the app, read the SDK's ref code and visitor id and send them to your backend with the purchase, beside the price.

2

On your backend, put them where your provider reads them. The table in Where the affiliate's code goes at checkout applies as it is: a checkout opened from an app is read the same way as one opened from a website. On Stripe Checkout that is the Checkout Session's own metadata, as affiliateo_ref and affiliateo_visitor_id, with your app ID as affiliateo_app_id. These are the same three keys the website SDK's getAttribution() fills.

1. In the app

Buy button (React Native)
import { Linking } from 'react-native';
import { useAffiliateRef } from '@affiliateo/react-native';

const { refCode, visitorId } = useAffiliateRef();

async function buy(priceId: string) {
  const res = await fetch('https://api.yourapp.com/checkout', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ priceId, refCode, visitorId }),
  });
  const { url } = await res.json();
  await Linking.openURL(url);
}

2. On your backend (Stripe Checkout)

Your backend
// POST /checkout: the app sends priceId, refCode and visitorId.
const { priceId, refCode, visitorId } = await req.json();

const session = await stripe.checkout.sessions.create({
  mode: 'subscription', // or 'payment'
  line_items: [{ price: priceId, quantity: 1 }],
  success_url: 'https://yourapp.com/thanks',
  cancel_url: 'https://yourapp.com/pricing',
  // On the session itself (not subscription_data), whatever the mode.
  metadata: {
    affiliateo_app_id: 'YOUR_APP_ID',
    ...(refCode ? { affiliateo_ref: refCode } : {}),
    ...(visitorId ? { affiliateo_visitor_id: visitorId } : {}),
  },
});

return Response.json({ url: session.url });
Taking the payment inside the app with Stripe's PaymentSheet? There is no Checkout Session then. Your backend creates the PaymentIntent, or the Subscription, and puts the same values in that object's own metadata, where affiliateo_app_id is required. See Stripe without Checkout.
No backend? A Stripe Payment Link takes the code as client_reference_id in its address, and a Polar Checkout Link as reference_id. Add the SDK's ref code to the link before the app opens it.

Renewals need nothing more: each one is credited to the affiliate of the subscription's first payment. A checkout without the code is recorded as organic, unless it is tied to a user you identified who clicked on another device (see One customer, two devices). One Stripe account can sell on your website and in your app at once: both land in the same program, each product at its own rate.

Store URLs

Direct users to the correct app store.

Add your iOS App Store URL and/or Google Play Store URL.
At least one store URL is required.
Users clicking a referral link are redirected to the correct store based on their device.

Embedded affiliate signup

Add a "Become an affiliate" button to your site and enroll your own users through the public API, without them ever leaving it.

1

Generate a read & write API key in your dashboard's API tab and keep it on your server. Never expose it in client-side code.

2

When a user clicks your button, send their email from your backend to POST /api/v1/businesses/{slug}/apps/{appId}/affiliates. The Affiliateo account is created automatically if it doesn't exist, and the response includes their short link, username link, and lifetime stats.

3

Show the links in your UI and render a QR code from the short link. Call the same endpoint again any time to refresh clicks, sales, and earnings. It never creates duplicates.

Full endpoint reference →

Giving affiliates free access to your app? Add app_email (the email they are signed in with in your app) to this same call. Nothing else is needed. How free access works

Data & privacy

Exactly what we read from your payment provider and your app, and what we don't.

Website sales

When a sale happens on your connected Stripe account, Stripe sends us a webhook event. We don't make API calls against your account or pull data on our own, we only read what's needed to attribute the sale and pay the affiliate.

What we read from each sale

Amount paid and currency
Sale type: one-time, subscription, trial, or renewal
Stripe's internal IDs (customer, subscription, payment intent), opaque pointers, not personal data
The affiliate attribution metadata your code attaches to the checkout session
The product name, only if you include it in checkout metadata

What we never read or store

Customer email, name, or phone number
Billing or shipping addresses
Card numbers or any payment method details
Tax IDs or VAT numbers
Line item details beyond an optional product name
Any other customer or transaction in your Stripe account

Stripe Connect doesn't offer a read-only scope for payment platforms, every Connect platform uses the same access level. We don't poll your account or run reports; we only listen for webhook events from sales that flowed through an affiliate-tagged checkout session.

Mobile app sales

When an in-app purchase happens in your app, RevenueCat sends us a webhook event. We don't poll your RevenueCat account or pull data on our own, we only read what's needed to attribute the sale and pay the affiliate.

What your RevenueCat API key is used for

List your RevenueCat projects so you can pick which one to attach
Create a single webhook integration in your project
Delete that webhook 120 days after you disconnect or pick another project (until then it only brings us refunds of earlier sales)
Verify the webhook still exists during periodic health checks

We never call subscriber, customer, entitlement, offering, or receipt endpoints. The key is only used to install and verify our own webhook.

What we read from each purchase event

Event type: initial purchase, renewal, one-time, trial, or refund
Gross sale price in USD
RevenueCat's transaction ID (an opaque pointer, not personal data)
Product ID and store (App Store or Google Play)
The affiliateo_ref subscriber attribute our SDK sets
RevenueCat's app user ID, only to recognize a customer who changes plan or comes back

What we never read or store

Customer email, name, phone, or any identity info
Device IDs (IDFA, IDFV, GAID) or IP addresses
RevenueCat aliases
Receipt data, purchase tokens, or store credentials
Entitlements, offerings, or other subscriber attributes
Any other customer or transaction in your RevenueCat account

RevenueCat doesn't currently offer scoped API keys for partners. We work around this by only making the four specific calls listed above and ignoring every other field RevenueCat sends in webhook payloads.

Payout schedule

When and how affiliates get paid.

Commissions are tracked in real time as purchases happen, on either rail. When they become payable follows the processor that carried the sale, because the commission comes out of the business owner's wallet and they need the money first: 14 days after the sale on Stripe, Shopify, WooCommerce, Whop and Polar, or the 1st of the next month for Paddle and App Store or Play Store sales.

1

Affiliate drives a sale, commission is recorded immediately.

2

Pending commissions become payable once the processor has settled the sale: 14 days for Stripe, Shopify, WooCommerce, Whop and Polar, or the 1st of the next month for Paddle and app store sales.

3

Business owner reviews and clicks "Pay" to transfer to affiliate wallets.

Mobile affiliate payouts are made from the business owner's wallet balance. Ensure sufficient funds are available.

Stripe payment processing fees apply separately. See the Fees page for full pricing details.