How to see which QR code brought each new app user
If your QR codes send people to the App Store or Google Play, BetterQR can tell you which code each new app user scanned, with no referral codes to type. Give every ambassador, poster or venue its own code and you can see installs and signups per code. This guide covers how it works for the people scanning, how to set it up, and what the results mean.
By Daniel Zaremba, founder of BetterQR · Updated
What it does
A normal app QR code can count scans, but the trail stops at the app store. You can see that 300 people scanned Jane's code, but not how many of them installed the app or signed up.
App install attribution follows the scan through the store. When someone opens your app for the first time, the app asks BetterQR which QR code they came from. BetterQR answers with the code, its campaign and tags, and how sure it is. Your app can then pass that to your own analytics, and tell BetterQR when the person signs up.
Who can use it
- An organization on the Enterprise plan. It uses the BetterQR API, which is part of Enterprise. See pricing.
- Mobile App QR codes. The code needs your App Store link, Google Play link and a website fallback.
- A small change to your app. Your developer adds one call on first launch. It takes about a day.
What people see when they scan
On Android, nothing changes. The scan goes straight to Google Play. BetterQR adds a hidden scan ID to the store link, which Google hands to your app after install.
On iPhone, people see a short “Get the app” page with your app name, an optional invite message (for example, “Jane invited you. Get a free first deal.”) and one button. Tapping it copies a link and opens the App Store. When your app first opens, iOS asks once whether it may paste from the clipboard. Apple doesn't pass anything through the App Store, so this copy step is how an iPhone install is matched exactly.
People who'd rather not copy can tap Continue without copying. People on a computer still go to your website fallback.
What the results mean
Every install gets one of three labels:
| Label | What happened | How much to trust it |
|---|---|---|
| Exact | The app found the scan ID from Google Play, or from the copied link on iPhone. | Certain. Safe to use for ambassador payouts. |
| Probable | No scan ID, but someone on the same network scanned one of your codes in the hour before the install, on the same kind of phone. Only used if you turn on network matching. | A good guess on home or venue Wi-Fi, weaker on mobile data. Use it for reporting, or pay a lower rate. |
| Organic | No scan could be linked to the install. | Not credited to any QR code. |
Each scan can credit only one install, so one shared link can't earn credit twice. If two different codes look equally likely for a probable match, BetterQR credits neither.
How to set it up
- Turn it on for your QR code. Create or edit a Mobile App QR code and switch on App install attribution. Add your app name and, if you like, an invite message for the iPhone page. Do this for each code you want to track, such as one per ambassador.
- Create an app key. Go to Settings → API keys, select Create key and choose the App (publishable) scope. Copy the key: it starts with
bqr_pk_and is shown only once. An app key can only report installs and events, so it's safe to put inside your app. Never put a normal API key in an app. - Send the key and this guide to your developer. The app needs to call BetterQR once on first launch, and optionally when someone signs up. The developer section below has the code.
- Decide on network matching. In Settings → API keys → App install attribution you can turn on Match installs by network. This adds the probable matches described above. It's off by default (see privacy below).
- Check your results. Open the QR code in your dashboard. The App installs card shows exact and probable installs, plus any events your app reports, such as signups.
For your developer
On first launch only, call POST /v1/app/open with the app key. Send the Google Play install referrer on Android, or the clipboard link on iPhone. Here's an Expo / React Native version:
import * as Application from "expo-application";
import * as Clipboard from "expo-clipboard";
import * as Device from "expo-device";
import { Platform } from "react-native";
// Run once, on the very first launch (keep a "done" flag in storage).
export async function reportFirstOpen() {
const ios = Platform.OS === "ios";
const body = {
installId: ios
? await Application.getIosIdForVendorAsync()
: Application.getAndroidId(),
platform: ios ? "ios" : "android",
osVersion: Device.osVersion ?? undefined,
language: Intl.DateTimeFormat().resolvedOptions().locale,
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
installReferrer: ios
? undefined
: await Application.getInstallReferrerAsync().catch(() => undefined),
// Only read the clipboard if it holds a link, to avoid a needless prompt.
clipboardUrl:
ios && (await Clipboard.hasUrlAsync())
? (await Clipboard.getUrlAsync()) ?? undefined
: undefined,
};
const res = await fetch("https://api.betterqr.app/v1/app/open", {
method: "POST",
headers: { "x-api-key": "bqr_pk_…", "content-type": "application/json" },
body: JSON.stringify(body),
});
return res.json(); // { match, qrCode, campaign, ... }
}The answer includes match (exact, probable or organic) and the QR code's name, slug, tags and campaign. Save these to your analytics as user properties (for example, PostHog $set_once) so later events are attributed too. Calling again for the same install returns the same answer, so retries are safe.
When the person signs up, report it:
await fetch("https://api.betterqr.app/v1/app/event", {
method: "POST",
headers: { "x-api-key": "bqr_pk_…", "content-type": "application/json" },
body: JSON.stringify({ installId, name: "signup", externalId: user.id }),
});You choose the event names, such as signup, first_order or first_booking. Use lowercase letters, digits and underscores, starting with a letter (up to 40 characters). Each name counts once per install, so sending it again is safe. An install can have up to 20 different names. The App installs card shows your six most common events.
Use your own user ID as externalId, never an email or name. The full reference, with every field and error, is in the API docs under “App attribution”.
Testing before launch
- Put a build on TestFlight (iPhone) or the Google Play internal testing track (Android). The store link in your QR code must point at that listing, because Google only passes the scan ID through Play.
- Uninstall the app from your test phone.
- Scan the QR code with the phone's camera. On iPhone, tap the button on the “Get the app” page.
- Install and open the app, and allow paste on iPhone. The first-open call should come back
exact, and the install appears on the QR code's App installs card.
Each test install needs a fresh scan, because a scan only credits one install. Reinstalling on the same phone gets the same answer as the first install.
Privacy and Apple's rules
- BetterQR never stores IP addresses. It keeps a one-way hash of the network address, with the phone type and OS version, for 30 days. After that the scan record is deleted and only the install result is kept.
- Install and event records hold only IDs: the install ID your app sends, and your own user ID if you send one. No names, emails or phone numbers.
- The iPhone copy step is something the person chooses to do, and the app reads the clipboard once with the system's permission prompt.
- Network matching is a grey area under Apple's rules on deriving data from a device. Many attribution tools use it, but it's off by default. Check it fits your App Store privacy answers before you turn it on, and declare attribution in your app's privacy label.
Troubleshooting
| What you see | Likely cause |
|---|---|
| iPhone users go straight to the App Store, with no page | Attribution is off for that code, or the iOS link isn't an apps.apple.com link. |
| Every install comes back organic | The app isn't sending the install referrer or clipboard link, or it runs the call after the first launch. Check that it runs once on first launch. |
403 with INSUFFICIENT_SCOPE | The app uses a normal API key. Create an App (publishable) key instead. |
404 INSTALL_NOT_FOUND on events | The event was sent before the first-open call, or with a different install ID. |
409 EVENT_LIMIT_REACHED on events | The install already has 20 different event names. Reuse existing names instead of putting values (like a plan or product) in the name. |
| Fewer probable matches than expected | Network matching is off, the install happened more than an hour after the scan, or the person switched from Wi-Fi to mobile data. |
Related
- Track billboard QR code scansTrack scans from each billboard separately: one dynamic QR code per site, clear naming, and a scan report you can compare week by week.
- OOH attribution for small brandsHow small brands measure billboards, posters and flyers: one QR code per placement, UTMs into GA4, and a simple cost-per-scan comparison.
- Poster A/B testing with QR codesTest two poster designs or offers in the real world: split placements, give each version its own QR code and read the result without guesswork.
- Measure billboard ROIMeasure billboard and poster ROI without an enterprise tool: cost per scan, scan-to-visit and visit-to-sale, worked through with example numbers.