QR Code Attribution: Offline-to-App Installs, Hop by Hop

QR code attribution works because an MMP-generated code carries a full tracking link, the same campaign, creative, and placement parameters a paid ad click would carry, so a scan can be attributed through deferred deep linking all the way to the install and the events after it. What you can measure is decided at print time, by the parameters baked into each code. What you lose happens at each hop between the camera and the first app open. And the losses aren't evenly distributed.

The hop-by-hop trace

Think of the path from a printed code to a measured install as a relay, not a single event. Each runner hands off a slightly thinner baton. Here's the whole run in one line per hop.

Camera scan routes to the OS, which decodes the modules and reads the raw URL (nothing about you exists yet). Short link resolve fires the tracking link, and every parameter you encoded gets logged server-side. Store redirect hands the user to the App Store, and almost no context survives the jump. Install and first open wakes the SDK, which asks the MMP "who was I before I existed?" Deferred match is where the MMP reconstructs intent and attributes the install to your placement.

And somewhere in there, the postback for the whole thing arrives. Sometimes seconds later, sometimes when it feels like it. We'll come back to that.

Hop 1, camera scan: the physical layer nobody instruments

This is the one hop that fails before a single line of your code runs, and it's the hop your dashboard will never show you. If the camera can't decode the symbol, there's no scan event, no link fire, no funnel entry. Just a person squinting at their phone in a parking lot.

Physical scanability is governed by error correction, defined in ISO/IEC 18004 and by DENSO WAVE, who invented the format. Four levels exist: L, M, Q, and H, recovering roughly 7%, 15%, 25%, and 30% of the codewords respectively. DENSO WAVE's own guidance says Level M (15%) is the most commonly chosen, and that you should pick Q or H for dirty factory environments while L suits clean, high-data situations. The U.S. patent literature puts the practical meaning plainly: a Level H code "will be scannable by a device even if 30% of the modules are damaged (soiled, washed out, faded, replaced with images)."

Here's the lever the marketing docs skip and the ISO spec makes obvious. Your URL length drives module density. Longer attribution URLs mean more modules packed into the same physical space, which means finer detail, which means worse scans on a curved can or a rain-warped poster. Higher error correction also adds modules. So you're spending a density budget in two places at once, and a bloated tracking link quietly eats your recovery margin. The docs don't frame it as a tradeoff. Testing on physical media shows it absolutely is one.

Once the camera reads the URL, the request hits your short link, and this is the exact moment your measurement either exists or doesn't. The link parameters are the measurement. There's no other hidden channel carrying your campaign context. If you didn't encode it here, it's gone.

Singular is blunt about what this granularity buys you: "This is how teams measure billboard-by-billboard performance, conference-by-conference signups, or packaging-batch-by-batch ROI." That sentence is the entire design constraint for offline QR. Every distinct thing you want to tell apart later needs its own minted link now. The MMPs (AppsFlyer, Singular, Adjust) generate QR codes directly on their deep-link infrastructure, so the code and the tracking link are the same object. Short-link tools do the same job with a different surface.

Hop 3, store redirect: the friction tax

Now the OS takes over and sends the user to the App Store. This hop charges a tax twice. First, plain drop-off: some people scan, see the store page, and bail before installing. Second, and this is the part engineers underestimate, there's no guaranteed pass-through of your context across the store boundary. The rich parameters you logged in Hop 2 do not ride along into the freshly installed app for free.

That gap is precisely why deferred matching has to exist. The store hands you a user with no memory of the scan that brought them there, and something has to stitch the two halves back together.

Hop 4, install and first open: the deferred match

On first open, the SDK fires and the MMP tries to reconnect this brand-new install to the earlier link resolve. AppsFlyer describes the OneLink behavior directly: "when a user taps a link and doesn't have the app installed, OneLink routes them through the app store and delivers them to the right in-app screen on first open." That's deferred deep linking, reconstructing intent across the install barrier.

But there's a hard floor, and it's worth internalizing before you plan any offline campaign. AppsFlyer's own support docs spell it out: without the SDK integrated, "it is still possible to create a working redirection link, but it will not be possible to deep link, deferred deep link, or measure installs in your dashboard." No SDK, no attribution. The redirect still sends people to the store, you just learn nothing. If you're still weighing whether the SDK plumbing is worth it, our take on when SKAN plus a warehouse is enough versus a full MMP covers that decision honestly.

What Apple's frameworks can and can't carry for QR code attribution

Here's where offline QR on iOS gets misunderstood. People assume SKAdNetwork will catch these installs. It mostly won't, and understanding why saves you a quarter of confused reporting.

SKAdNetwork is aggregated install attribution for ad campaigns, full stop. Branch's documentation states it directly: "SKAdNetwork does not provide attribution for owned and organic installs. The SKAdNetwork attribution method is specifically for app install campaigns." An offline QR scan with no click through an ad network lands in the owned/organic bucket, which is exactly the bucket SKAN ignores. So most of your poster and packaging installs are, from SKAN's point of view, invisible by design.

AdAttributionKit doesn't rescue you either. A QR scan by an existing user resembles a re-engagement flow, and per Apple's WWDC 2024 session, "re-engagement conversions can only have the click ad interaction type. View-through re-engagement is not supported," with the first conversion-value update required "within 48 hours of the conversion." On top of that, these frameworks can't move your routing data. As WarpDriven put it in their 2025 write-up on deferred-deep-link pitfalls, "SKAN/AdAttributionKit cannot transport deep-link context; relying on them for personalization breaks routing."

The diagnostic that actually helps: separate your two failure modes. Mode 1 is scan-to-install match-rate loss, meaning redirect friction, fingerprint reconstruction weakening, and no ATT consent. Mode 2 is framework blind spots, meaning SKAN skipping owned/organic and AdAttributionKit's click-only, 48-hour, context-free window. When an offline number looks wrong, ask which mode you're in first. A Mode 1 problem is fixable with better links and SDK hygiene. A Mode 2 problem means you're measuring an owned channel and should stop expecting an ad framework to report it. Conflating the two is how teams spend a week debugging plumbing that was never the issue.

If ATT consent is your bottleneck, the mechanics in our mobile consent management guide matter more here than any framework tuning.

Worked example: packaging vs poster vs TV screen

Let me make this concrete with a small illustrative dataset. The physical constants (error-correction recovery percentages) are from DENSO WAVE and the USPTO patent text. The funnel numbers below are illustrative, not sourced measurements. They're here to show why one blended number hides three different stories.

Say you run 30,000 scans across three placements. Uniqode's 2025 analysis of over 188 million scans is the kind of named-methodology benchmark you'd anchor real targets against. Treat mine as directional shapes, not their figures.

Curved package (a can, printed at Level Q, ~25% recovery to survive gloss and curvature). 10,000 scans. The physical read is decent but the codes are denser to hit Q, so if your URL was long you'd lose reads here. Assume a 62% scan-to-install match after redirect friction: about 6,200 measurable installs.

Weathered outdoor poster (Level H, ~30% recovery, because it will fade and get rained on). 10,000 scans. High error correction keeps it readable when 30% of modules are damaged, but every scan happens outdoors, often at distance, and match rates suffer from the ATT and fingerprint side. Assume 48% match: about 4,800 measurable installs.

Glossy TV screen (Level M default, ~15% recovery, since a clean, self-lit surface needs little). 10,000 scans. The surface is easy to read, but people scan a TV from across a room and half of them are on a shared living-room device with no consent. Assume 41% match: about 4,100 measurable installs.

Blended, that's 15,100 installs on 30,000 scans, a tidy 50.3%. Report only that number and you'd conclude the campaign is fine. Split it and you see the TV screen bleeding attribution not because the code scans poorly but because of the environment and consent, while the poster's issue is a different mix entirely. Same 50%, three unrelated problems.

Placement Ideal EC level Scan-reliability risk Cleanly measurable per hop?
Curved package Q (~25%) Curvature + gloss; density-sensitive to long URLs Yes, if per-batch link is unique
Weathered poster H (~30%) Fading, dirt, distance scans Scan-to-install match weakest
Glossy TV screen M (~15%) Read is easy; shared-device + no ATT hurts match Owned-channel blind spot on iOS

The mistake that quietly destroys your readouts

One shared QR code across packaging, poster, and TV. That's the error that costs teams the most, and it happens because a single code is easier to design and reprint.

The problem is irreversible. Recall Hop 2: the link parameters are the granularity. If all three placements resolve the same link, every scan lands in one undifferentiated bucket, and there's no post-hoc analysis that unmixes them. You can't infer from a timestamp whether a scan came from a can or a billboard. The information was never captured, and no dashboard filter invents it later. I once watched a team argue for an hour about which offline placement "worked" when the honest answer was that their own encoding made the question unanswerable.

The fix is cheap and non-negotiable: a unique code per placement, each with its own UTM and parameter tagging. Standard MMP guidance treats this as the baseline setup, not an optimization.

Setup checklist: minting per-placement codes

Do these in order, because the order is load-bearing.

  1. Integrate the SDK first. Per AppsFlyer's docs, without it you can build a working redirect but measure zero installs. Everything downstream assumes this is done.
  2. Generate one link per placement. The MMPs (AppsFlyer, Singular, Adjust) mint QR codes on their own deep-link infrastructure, including deferred deep links. Short-link tools are another route to the same per-placement links: Kixo offers kixo.cc short links with deferred deep linking as part of an AI-native, chat-first product and marketing analytics platform, where you ask questions in plain language and get dashboards back. It's newer and analytics-native rather than a long-established MMP, so weigh it as one option among several and check it against your existing stack. If you're comparing routing behavior and fallbacks across providers, our deep linking platforms comparison is the place to start.
  3. Keep URLs short. Callback to Hop 1: shorter links mean lower module density, which means better scans on curved and weathered media.
  4. Tag every placement uniquely. UTM plus parameters, no reuse, ever.

If you're mid-provider-switch while planning offline work, sequence it carefully. Our MMP migration playbook exists so you don't strand these codes pointing at a dead endpoint.

What you genuinely cannot measure

Some things on this path aren't hard, they're impossible, and pretending otherwise burns credibility with your finance team.

You cannot measure view-through offline exposure, the person who saw the poster, didn't scan, and installed later from memory. AdAttributionKit doesn't support view-through re-engagement at all, and nothing else observes an un-scanned poster. You cannot reliably attribute cross-device scans without ATT consent, which knocks out a chunk of the shared-TV case. And on iOS, an offline QR install with no ad-network click is an owned/organic install that SKAN explicitly does not attribute, per Branch's documentation.

The postback, meanwhile, will still arrive whenever it feels like it. Occasionally before you've finished your coffee, occasionally long after you've closed the report and moved on. Plan your dashboards for late arrivals, not on-time ones.

Measure the placements you differentiated well, and accept the ceiling on the rest. A campaign with three cleanly separated codes and an honest note about the un-measurable tail beats a single blended number that pretends the ceiling isn't there.