Skip to content
CodeOnAir
← All posts

Build a refer-and-earn flow in React Native with deferred deep links

A personal invite link for every user, the referral code kept through the App Store or Play Store install, and the reward credited on your server from a signed webhook, with the abuse checks built in.

CodeOnAir team27 October 20269 min read

"Refer and earn" sounds like a small feature: each user gets a link, a friend installs the app, both get a reward. Building it shows where it gets hard. The referral code has to survive a trip through the App Store or Play Store, the reward has to be credited somewhere people can't fake it, and the programme has to cope with people trying to refer themselves.

This guide builds the whole flow in React Native with @codeonair/deeplink-react-native: a personal invite link per user, the referral read on the first open after install, and the reward credited on your server from a signed webhook.

How a referral link flow works, end to end

Asha taps "Invite friends"  →  the app gets her personal link
          ↓
Asha shares it on WhatsApp  →  Ravi taps it
          ↓
Ravi doesn't have the app   →  Play Store / App Store  →  install
          ↓
Ravi opens the app for the first time  →  the app receives ref=ASHA50
          ↓
Ravi signs up  →  your server credits Asha and Ravi (once)

Two parts of this need care: the arrow through the store (the code has to survive the install) and the last step (the reward has to be decided on your server).

Why the code gets lost at install, and how deferred deep linking keeps it

If Ravi already has the app, a Universal Link or App Link opens it, and the app reads the code from the URL. That's the easy case.

If he doesn't, the link goes to the store, and the store doesn't pass your URL to the app. On first launch, React Native's Linking.getInitialURL() returns null. The code is gone.

Deferred deep linking fills that gap. The link server records the tap, sends Ravi to the store, and when the app first opens, matches that open to the tap:

  • Android: the Play Store returns an install referrer containing the click ID, so the match is exact.
  • iOS: there's no referrer, so the match uses signals such as the network address, OS version, language and time, within the link's attribution window. Here's how iOS matching works without the IDFA, and where it fails.

Step 1: Create a personal invite link for each user

Install the SDK and, on Android, the install referrer package the SDK uses for exact matches:

npm install @codeonair/deeplink-react-native @react-native-async-storage/async-storage
npm install react-native-play-install-referrer
cd ios && pod install

Add your App ID to AndroidManifest.xml (com.codeonair.APP_ID) and Info.plist (CodeOnAirAppId), and set up Associated Domains and the App Links intent filter as in our React Native deep linking guide. Then create the link once per user and store it:

import DeepLink from "@codeonair/deeplink-react-native";

DeepLink.init();

async function getInviteLink(user: { id: string; referralCode: string; firstName: string }) {
  const saved = await api.getInviteLink(user.id);
  if (saved) return saved; // reuse it: one link per user, not one per share

  const url = await DeepLink.createLink({
    path: "/invite",
    params: { ref: user.referralCode },
    campaign: "referral",
    ogTitle: `${user.firstName} sent you ₹50`,
    ogDescription: "Install the app and get ₹50 off your first order.",
    ogImage: "https://example.com/img/invite-card.png",
  });
  await api.saveInviteLink(user.id, url);
  return url;
}

createLink posts to the link API with your App ID, so no secret key ships in the app. You can also pass slug: user.referralCode for a readable URL such as yourapp.codeonair.com/ASHA50; if that slug is taken, the call fails and you can retry without it.

Many teams create the link on the server instead, at sign-up, with the REST API and a secret API key:

curl -X POST https://app.codeonair.com/v1/links \
  -H "Authorization: Bearer sk_live_…" \
  -H "Content-Type: application/json" \
  -d '{ "path": "/invite", "params": { "ref": "ASHA50" }, "campaign": "referral" }'

Plan limits matter here. One link per user counts against your plan's app links, however it's created. The Free plan has 100, which is enough to build and test the flow. A real programme needs a paid plan, where links are unlimited (pricing). New Free workspaces can also create at most 30 links an hour and 100 a day during their first seven days.

Step 2: Share it

React Native's Share API opens the system share sheet, which includes WhatsApp, Telegram, SMS and the rest:

import { Share } from "react-native";

async function invite(user) {
  const url = await getInviteLink(user);
  await Share.share({
    message: `I use Shopwise for groceries. Get ₹50 off your first order: ${url}`,
  });
}

To open WhatsApp directly, WhatsApp's own share URL works: Linking.openURL("https://wa.me/?text=" + encodeURIComponent(message)).

When the link is pasted into a chat, the messaging app's preview crawler fetches it. CodeOnAir recognises preview crawlers (WhatsApp, Telegram, Slack, Facebook and others), serves them the link's title, description and image, and doesn't count them as clicks. So the ogTitle you set is what Ravi sees in the chat.

Step 3: Read the referral on first open

onLink fires for every link the app receives: a Universal Link or App Link when the app is installed, a URI scheme link, and the deferred link on the first launch after an install. Each comes with fields that say how it arrived:

Field Meaning
path, params What you set on the link: /invite, { ref: "ASHA50" }
isDeferred true when the link was recovered after an install
isFirstSession true on the first launch after install
matchMethod install_referrer, clipboard, probabilistic, universal_link or uri_scheme
confidence 1 for exact matches, lower for probabilistic ones

Save the referral until the person signs up:

import AsyncStorage from "@react-native-async-storage/async-storage";

useEffect(
  () =>
    DeepLink.onLink(async (link) => {
      if (link.path === "/invite" && link.params.ref) {
        await AsyncStorage.setItem(
          "pendingReferral",
          JSON.stringify({ ref: link.params.ref, deferred: link.isDeferred, method: link.matchMethod, confidence: link.confidence }),
        );
        navigation.navigate("Welcome", { invitedBy: link.params.ref });
        return;
      }
      navigation.navigate(link.path, link.params);
    }),
  [],
);

Show the invite on the first screen ("Asha sent you ₹50"), but treat what the app received as a hint for the UI. The decision to pay out belongs on the server.

Step 4: Credit rewards on your server, not in the app

If the app tells your API "I was referred by ASHA50", anyone with a proxy can send the same request. Instead, let your server hear about the install from CodeOnAir.

Add a webhook in the dashboard for app.install and app.event. When a new install opens the app, your endpoint receives:

{
  "id": "evt_…",
  "type": "app.install",
  "created_at": "2026-10-27T09:12:44.000Z",
  "data": {
    "device_id": "4f1c…",
    "platform": "android",
    "matched": true,
    "method": "install_referrer",
    "confidence": 1,
    "click_id": "clk_…",
    "link_id": "lnk_…",
    "link": {
      "url": "https://yourapp.codeonair.com/k3P9xQa",
      "slug": "k3P9xQa",
      "deep_link_url": null,
      "path": "/invite",
      "params": { "ref": "ASHA50" },
      "campaign": "referral",
      "utm": {},
      "variant": null
    }
  }
}

Then, after sign-up, the app reports a conversion for the same device:

await DeepLink.setUserId(user.id); // attaches your user id to this install
await DeepLink.trackEvent("signup", { user_id: user.id });

That arrives as an app.event webhook with the same device_id. Your server joins the two: this install came through Asha's link, and this account signed up on it.

Every delivery is signed. The CodeOnAir-Signature header looks like t=<unix seconds>,v1=<hex>, where v1 is an HMAC-SHA256 of "<t>.<raw body>" with your webhook's secret. Verify it on the raw body before parsing:

import express from "express";
import { createHmac, timingSafeEqual } from "node:crypto";

const app = express();

app.post("/webhooks/codeonair", express.raw({ type: "application/json" }), async (req, res) => {
  const header = req.get("codeonair-signature") ?? "";
  const parts = Object.fromEntries(header.split(",").map((p) => p.split("=") as [string, string]));
  const expected = createHmac("sha256", process.env.CODEONAIR_WEBHOOK_SECRET!).update(`${parts.t}.${req.body}`).digest("hex");
  const fresh = Math.abs(Date.now() / 1000 - Number(parts.t)) < 300;
  if (!fresh || !parts.v1 || parts.v1.length !== expected.length || !timingSafeEqual(Buffer.from(parts.v1), Buffer.from(expected))) {
    return res.status(400).end();
  }

  const event = JSON.parse(req.body.toString("utf8"));
  if (await db.eventSeen(event.id)) return res.status(200).end(); // retries carry the same id

  if (event.type === "app.install" && event.data.matched && event.data.link?.path === "/invite") {
    await db.saveInstallReferral(event.data.device_id, event.data.link.params.ref, event.data.method, event.data.confidence);
  }
  if (event.type === "app.event" && event.data.name === "signup") {
    await rewards.creditOnce({ deviceId: event.data.device_id, newUserId: event.data.data.user_id });
  }
  await db.markEventSeen(event.id);
  res.status(200).end();
});

Failed deliveries are retried with backoff for about a day and a half, so make the handler idempotent: the event id stays the same across retries. Webhooks are on every plan: 1 on Free, 2 on Starter, 10 on Growth and 50 on Scale.

Stopping abuse

Referral programmes attract people who refer themselves. Build the rules in from the start:

  • Use the match method. install_referrer and clipboard are exact. probabilistic is a best guess, and on shared Wi-Fi or mobile carrier networks it can credit the wrong person. Pay automatically for exact matches, and for probabilistic ones set a threshold (for example confidence >= 0.8) or ask the user to confirm who invited them.
  • Reward an action, not the install. Credit after a verified sign-up (phone OTP) or a first order, not on first open.
  • One reward per account, and per phone number or payment method. The SDK's install ID is reset when the app is uninstalled, so a reinstall looks like a new install. Don't use it as your only key.
  • No self-referrals. Reject a referral when the new account's phone, email or payment details match the referrer's.
  • Cap and delay. Limit rewards per referrer per day, and release them after a short holding period, so you can reverse obvious fraud.

Testing the flow on Android and iOS

  • Android, exact match: put a build on a Play test track. Uninstall the app, tap the invite link on the phone, install from the Play Store, and open the app. matchMethod should be install_referrer. A sideloaded APK has no Play install referrer, so it falls back to probabilistic matching.
  • iOS: delete the app, tap the link in Notes or Messages, install from TestFlight or the App Store, and open it within the link's attribution window (up to 1 hour on Free, 24 hours on Starter and Growth, 7 days on Scale). Stay on the same network for the tap and the first open.
  • iOS clipboard matching (optional): turn on advanced deferred deep linking in your app's settings and pass a clipboard getter, for example DeepLink.init({ getClipboardString: () => Clipboard.getStringAsync() }) with expo-clipboard. iOS asks the user to allow the paste.
  • Tap a fresh link for each test. A click is matched to one install at most.
  • Run with DeepLink.init({ debug: true }) and log matchMethod and confidence, and check that your webhook endpoint receives app.install.

For the commands to test links in general, see our deep link testing guide.

FAQ

Can I give the reward before the friend signs up?

You can show it straight away ("₹50 is waiting for you"), but credit it after a verified sign-up or first order. Crediting on install pays for uninstall-and-reinstall loops.

How accurate is referral matching on iOS?

Without clipboard matching, iOS matches are probabilistic and come with a confidence score. They're usually right when the tap and the first open happen on the same network soon after each other, and can be wrong on shared networks. Turn on clipboard matching for exact matches, or confirm low-confidence ones with the user.

Do I need a backend?

For a real programme, yes. The app can show the invite, but the reward should be decided by your server from signed webhooks, so it can't be faked from a phone.

Can the same link be used by many friends?

Yes. Each user's link can be shared with any number of people, and every install it brings is matched separately. That's why one link per user is enough.

What if the friend already has the app installed?

The link opens the app directly, isDeferred is false, and no app.install event is sent. Most programmes don't reward existing users, but you can still show the inviter's name or apply an offer.

Add referral links to your React Native app

npm install @codeonair/deeplink-react-native @react-native-async-storage/async-storage
npm install react-native-play-install-referrer

Follow the React Native SDK docs, then create a free account to build and test the flow with 100 app links. See AppLinks for everything links do.

Your first AppLink in five minutes.

Create an app, pick a subdomain, and share a link that opens your app. Try Growth free for 30 days — no card needed.