Guides
Mobile apps and installs
This guide covers React Native and Expo apps. A referral link or QR code has to reach the app in one of three ways:
- The app is installed. The link opens it as a universal link, app link or custom scheme, and the app captures the link.
- The app is not installed. The link opens your website, the website captures the visit and asks for an install claim, and the app redeems the claim after install. No third party is involved.
- You already use Branch or AppsFlyer. Their deferred deep link carries the referral through the install, and the app reads it with an SDK helper.
Grizzlee never matches devices by IP address, user agent or any other fingerprint. Each path above hands over the referral explicitly.
Program kind does not restrict mobile tracking. tracking.links controls native in_app captures, tracking.qrCodes controls QR captures and tracking.attributionCodes controls typed codes. Enable typed codes for an existing affiliate program without changing kind. Provider promo codes are separate discounts in Stripe or Shopify; the provider order adapter attributes their use without a saved receipt.
Set up the app
npm install @grizzlee/sdk @react-native-async-storage/async-storageOpen Settings, then Developers. Edit the Default tracking key, or create a new one, and turn on native capture. Add your website origins to the same key, or create a second key for the website. A publishable key can capture and redeem referrals and resolve placements. Never ship a server API key in an app.
import AsyncStorage from "@react-native-async-storage/async-storage";
import { GrizzleeTrackingClient } from "@grizzlee/sdk/tracking";
export const tracking = new GrizzleeTrackingClient({
baseUrl: "https://app.grizzlee.ca",
publishableKey: process.env.EXPO_PUBLIC_GRIZZLEE_PUBLISHABLE_KEY!,
storage: AsyncStorage,
});expo-secure-store works too. Its keys allow only letters, digits, ., - and _, so encode the SDK's keys before you pass them through. At checkout, send every stored receipt and any typed referral code together to your backend. Your backend builds the candidates list and chooses the customer from authentication. Grizzlee picks the newest valid candidate while customer attribution is open, or for an order-only program. A qualified customer attribution keeps its partner. Grizzlee reports skipped candidates with reason codes. An install claim keeps its original scan time. A typed code uses checkout time and beats an older scan.
Resolve a placement
Use a stable key such as customer_referral in the app. The merchant's server points it at a program with setPlacement, or the merchant does it in the dashboard under Developer, Program keys.
const placement = await tracking.resolvePlacement("customer_referral");
const referral = await tracking.receipt(placement.programId);resolvePlacement returns { placement, programId, active } through the publishable-key API. A paused program returns active: false. An unknown key returns 404. Resolve when the screen opens, so moving the key to another program needs no new app build. Capture and install claim redemption responses include the keys currently mapped to the captured program in placements: string[], or [] when none exist. Existing stored receipts may leave this field out.
App already installed
Point referral links at a path your app handles as a universal link (iOS) or app link (Android), such as https://example.com/r?ref=KARL7Q. Feed every incoming URL to the SDK:
import { Linking } from "react-native";
const open = (url: string | null) =>
url ? tracking.captureFromUrl(url).catch(() => null) : null;
void Linking.getInitialURL().then(open);
Linking.addEventListener("url", ({ url }) => void open(url));readReferralLink and captureFromUrl read the query by hand when URLSearchParams.get is missing, as in React Native, so no URL polyfill is needed. They read ref, program and channel, and the grizzlee_ref, grizzlee_program and grizzlee_channel names of older links. A link with only ref sends the page's origin and path as destination, so Grizzlee picks the partner's program whose signup page or destinationUrl is that page. Point the universal link at that page, or include program. When ref names no partner, captureFromUrl returns null.
App not installed: install claims
When the app is not installed, the universal link opens the web page at the same address. That page is the landing page. It captures the visit like any website, asks for an install claim, and sends the visitor to the store.
import {
GrizzleeTrackingClient,
createPlayStoreUrl,
readReferralLink,
} from "@grizzlee/sdk/tracking";
const web = new GrizzleeTrackingClient({
baseUrl: GRIZZLEE_ORIGIN,
publishableKey: GRIZZLEE_WEB_KEY,
storage: window.localStorage,
});
const referral = readReferralLink(window.location.href);
if (referral) {
const captured = await web.capture({
...referral,
installClaim: true,
destination: window.location.origin + window.location.pathname,
});
const code = captured.installClaim?.code; // "K7Q2-M9XD-4HPA"
const playStore = code
? createPlayStoreUrl({ packageName: "com.example.app", installClaim: code })
: PLAIN_PLAY_STORE_URL;
// Show the Play Store button with playStore and the App Store button with your App Store URL.
// On iOS, also show code with a copy button. See below.
}The claim code works once and expires after 7 days, or sooner when the program's referral window ends. Only one click is recorded for the scan, whether the visitor buys on the website or in the app. The first customer to claim it owns it.
Android: Play Install Referrer
The Play Store passes the referrer value from the store link to the app after install. createPlayStoreUrl puts grizzlee_claim=<code> there. At first launch, read it and hand it to the SDK:
// Expo: expo-application. Bare React Native: react-native-play-install-referrer.
import * as Application from "expo-application";
import { Platform } from "react-native";
if (Platform.OS === "android") {
const referrer = await Application.getInstallReferrerAsync().catch(
() => null,
);
await tracking.captureInstallReferrer(referrer).catch(() => null);
}With react-native-play-install-referrer, call PlayInstallReferrer.getInstallReferrerInfo((info, error) => ...) and pass info.installReferrer. Install the module into your Expo project or bare React Native app. Use a Play Store installation to check the store's referrer handoff. See Expo Application and Google's Install Referrer API.
The Play Store returns the same referrer string on every launch for about 90 days. captureInstallReferrer remembers the string it handled in the client's storage, so later launches do nothing. The referrer's visit happened before the install, so it never replaces a receipt the app already stored for the same program, even one captured while the request was pending. A claim the server refuses is remembered too. Network errors and HTTP 429 throw and are retried on the next launch.
A store link can also carry a referral directly, without a landing page. createPlayStoreUrl({ packageName, referral: { referralCode, programId, channel: "qr" } }) builds a Play Store QR target, and captureInstallReferrer captures it at first launch.
iOS: a code the visitor moves
The App Store has no install referrer. Grizzlee does not guess, and the app does not read the clipboard on its own. The landing page shows the claim code with a button that copies it, next to the App Store button:
await navigator.clipboard.writeText(code); // only on the visitor's tapAt first launch, or at checkout where you already ask "Have a referral code?", accept the code. A paste button that the person taps is fine. Decide which call to make with isInstallClaimCode:
import { isInstallClaimCode } from "@grizzlee/sdk/tracking";
import { GrizzleeApiError } from "@grizzlee/sdk/errors";
if (isInstallClaimCode(typed)) {
try {
await tracking.redeemInstallClaim(typed);
return; // The receipt is stored. Checkout sends it as usual.
} catch (error) {
if (!(error instanceof GrizzleeApiError) || error.status !== 404)
throw error;
// An unknown, used or expired claim may still be a partner code.
}
}
await sendTypedCodeToBackend(typed); // Your backend adds it to checkout candidates.Claim codes are 12 characters of Crockford base32 in groups of four, such as K7Q2-M9XD-4HPA. Case, spaces and hyphens don't matter. A partner code can look the same, so fall back to the partner code when redemption fails.
Branch
Create links whose link data has these keys. See Branch's React Native reference.
| Key | Value |
|---|---|
grizzlee_ref | The partner's referral code |
grizzlee_program | The Grizzlee program ID |
grizzlee_channel | Optional: link, qr, coupon or in_app |
ref, program and channel work too, with ref holding the partner's short code. A Branch open has no page to match, so leave out the program only for a partner with one linkable program.
In the Branch dashboard, add them under the link's deep link data when you create a quick link, or pass them in the link data from the Branch SDK or API. Query parameters added to a Branch link also arrive in its data. Point QR codes straight at the Branch link, not at your landing page, so the visit is not captured twice.
import branch from "react-native-branch";
import { readBranchReferral } from "@grizzlee/sdk/tracking";
branch.subscribe(async (event) => {
const referral = readBranchReferral(event);
if (referral) await tracking.capture(referral).catch(() => null);
});Use subscribe, which fires for each link open including the first open after install. It also reports ordinary universal links through +non_branch_link, so with Branch drop the Linking listener from above, or every open records two clicks. Don't read getFirstReferringParams on every launch. It returns the same install link forever, so each launch would capture it again.
readBranchReferral takes the subscribe event or its params. It returns null when Branch reports an error or +clicked_branch_link is false. It reads the grizzlee_ keys from the link data first, then from the query of +non_branch_link, ~referring_link, +url, $canonical_url and $deeplink_path.
AppsFlyer
Deferred deep links for new installs carry only deep_link_value and deep_link_sub1 to deep_link_sub10. See AppsFlyer's React Native guide. Set these on the OneLink link or template:
| Parameter | Value |
|---|---|
deep_link_sub1 | The partner's referral code |
deep_link_sub2 | The Grizzlee program ID. Optional when the partner has one linkable program |
deep_link_sub3 | Optional: link, qr, coupon or in_app |
Already using these subs? Pick others and pass them as keys, for example readAppsFlyerReferral(result, { keys: { referralCode: "deep_link_sub5", programId: "deep_link_sub6", channel: "deep_link_sub7" } }). A full referral link URL-encoded in deep_link_value also works, and the grizzlee_ keys are read first when the payload has them, as it does for an installed app.
import appsFlyer from "react-native-appsflyer";
import { readAppsFlyerReferral } from "@grizzlee/sdk/tracking";
appsFlyer.onDeepLink(async (result) => {
const referral = readAppsFlyerReferral(result);
if (referral) await tracking.capture(referral).catch(() => null);
});Register onDeepLink before initSdk. readAppsFlyerReferral takes the onDeepLink or onInstallConversionData result, its data or deepLink, or the JSON string Android can deliver. It returns null when the deep link was not found, for organic installs, and when is_first_launch is false. Conversion data repeats on every launch, so that last check keeps it from being captured again.
What Grizzlee records
Every path ends in the same receipt and the same click record. The channel is the one on the link or QR code. Your backend claims the receipt with its private key, by POST /referrals/claim or with the order, exactly as for a web capture. See the API reference.