How to track QR code scans — the data primer
QR scans get tracked when the code points at a redirect URL instead of the destination directly. The scan event lifecycle and what data you actually get.
QR scans get tracked when the code points at a redirect URL instead of the destination directly. That single design choice is the whole game. A static QR with https://yourshop.com/spring baked into the image will land the visitor on your site but tells your server nothing the visit doesn't already say — same as any other URL hit. A dynamic QR with https://yourdomain.com/p/spring baked into the image hits your redirect layer first, logs the click with everything the redirect server can see, then issues a 302 onward to https://yourshop.com/spring. The destination experience is identical. The data on the way is night and day.
This post is the primer on what that redirect actually captures, what it can't, and how QR analytics compare to the web analytics that fire after the visitor lands. It's the companion to conversion tracking with QR codes and short links — that piece walks the full scan-to-revenue chain; this one stays on the first hop. Once you're comfortable with the high-level captured-vs-not split, the field-by-field deep dive in QR code analytics — what every scan can tell you walks each row column in turn, including the bot-filter tells and the UTC time-zone trap. If you've ever wondered why scan dashboards show "iPhone, 14:02, somewhere in Toronto" but not the name of the person scanning, this is the explanation.
The scan event lifecycle in four steps
Every tracked scan goes through the same four stages. Understanding the order makes the rest of this post land.
Step 1 — the scan itself. The phone's camera or a dedicated scanner app decodes the image into a URL string. This step is entirely on the device. Your server learns nothing. Static QR codes stop here in terms of your visibility — they decode to the destination URL and the phone navigates straight there, bypassing any tracking layer you might wish existed.
Step 2 — the redirect request. For a dynamic QR (the kind that points at a short URL on your domain), the phone now makes an HTTP request to your redirect endpoint. This is the moment your server first hears about the scan. The request brings everything an HTTP request always brings: headers, the requesting IP, the path, sometimes a referrer, sometimes nothing else.
Step 3 — the recording. Your redirect server logs the click before returning the response. Most platforms write a row with a click id, the slug, the timestamp, the IP, the user agent, the referrer if present, any UTM parameters that ride along, and a derived rough geo. This row is the only place a scan event becomes durable data. Skip this step and the scan is invisible.
Step 4 — the 302. The server returns a 302 Found (or sometimes 301) with a Location header pointing at the real destination. The browser follows it. The visitor lands on the destination page. From their perspective the redirect was invisible — a tap on the QR, a half-second pause, the destination loads.
The whole chain takes about 80-200 ms on a normal phone connection. Fast enough that nobody notices, slow enough to do real work. The analytics docs walk through the dashboard view of what that row looks like once it's recorded.
What the redirect can capture
Six fields make up the bulk of useful scan data. Everything else is a derivative.
Timestamp. Server-side wall clock at the moment the redirect hit. Down-to-the-millisecond precision is normal. From the timestamp you can derive day-of-week, hour-of-day, time-since-launch, and time-to-conversion for the destination event. The campaign-pacing analysis in real-time link analytics lives entirely off this field — every "clicks per minute" chart is just timestamps bucketed in two-second windows.
IP-derived rough geo. The IP address arrives in the request envelope. Your platform runs it through a geolocation database — MaxMind GeoIP2 is the canonical one — and stores country, region, and city. The accuracy: country is right ~99% of the time, region is right ~80%, city is right ~55-65%, and "right" for city often means "the city the carrier's local PoP is in", not necessarily the visitor's actual city. For a mobile scan over a cellular network the precision is roughly "metropolitan area". Carriers route mobile traffic through a small number of regional egress points, so a scan from a suburb 30 km from a major city often geolocates to the city centre.
User-agent device and OS. The User-Agent header announces the browser, the OS, and (with a few hints) the rough device class. From it your server can derive: device type (phone / tablet / desktop), operating system (iOS / Android / other), browser (Safari / Chrome / in-app), and on iOS, the specific model family in a narrow range (iPhone 12 vs iPhone 15 isn't usually distinguishable; iPhone vs iPad vs Mac is). Android is fuzzier because manufacturers customise the UA string; the OS version is reliable, the exact device less so.
Referring placement when tagged. This is the one field you control entirely. UTM parameters on the redirect URL — utm_source=poster, utm_medium=print, utm_campaign=spring — ride into the redirect and into your log row. They tell you what surface the scan came from, because the surface is what you tagged. The QR on the poster carries a different utm_source than the QR on the magazine page; the redirect log carries both, separately. The UTM parameters that actually matter post covers the convention, and the beginner version is in what is a UTM parameter for the five-field tour.
Slug as implicit identifier. Even without UTMs, the slug itself — /p/spring-poster-NYC — uniquely identifies the placement if you generated unique slugs per surface. The slug is belt and braces against landing pages that strip query parameters.
Referrer (when present). Most QR scans land on the redirect with no Referer header at all — the phone's camera app isn't a browser, so there's no referring page. Scans that originate from a screenshot a friend shared in a chat app sometimes carry a chat-app referrer; scans of a QR rendered on a web page carry the originating page. The signal is weak but occasionally tells you the QR was screenshotted and reshared.
What the redirect cannot capture
Most of what people assume is captured isn't. The gap matters because expectations shaped by web-analytics or app-attribution coverage don't translate cleanly.
The scanning app's name. When iOS scans a QR through the Camera app, the request that hits your server comes from Safari (or the user's default browser). When iOS scans a QR through a third-party app, the request still comes through the user's default browser after iOS opens the link. From your server's vantage point, "scanned via Camera" and "scanned via a third-party scanner" look identical — both are Safari hits with no fingerprint distinguishing them. Same on Android: Google Lens, the Camera, and Chrome all hand the URL off to Chrome (or the default browser) before the network request fires. The handoff hides the originating app.
The exception: some in-app browsers (Instagram, TikTok, Slack) preserve their own user-agent string when opening a link, so you can tell a scan came from inside Instagram if Instagram itself scanned and opened the link. But for true camera-app scans of a printed QR, the originating app is invisible.
Exact GPS coordinates. A web browser does not send GPS data with HTTP requests. Period. The only way to get a real GPS fix is to land the visitor on a page that requests location permission (the JavaScript navigator.geolocation API), and the visitor has to grant it explicitly. Even then, the fix happens after they land — it's destination-page data, not scan-event data. The "geo" you see in QR analytics is always IP-derived rough geo, never satellite-grade coordinates.
The visitor's identity. A scan event has no notion of "who" — only "this IP, this user agent, this timestamp". Joining a scan to a person requires the visitor to do something identifying on the destination side: sign in, fill a form, claim a discount code tied to the click. Until that happens, the scan is anonymous. This is the structural reason that conversion tracking (the conversion tracking with QR codes and short links piece) is a separate layer on top of scan tracking — it joins the anonymous scan to the identified conversion via a click id that travels through the funnel.
Whether the scan happened indoors, outdoors, in a store, on a billboard. The surface medium isn't in the HTTP request. You know the surface only because you tagged it — the utm_source is something you wrote into the link, not something the camera communicated. The same QR on a window decal and a flyer is indistinguishable in the log unless you printed them with different slugs or UTMs.
Other apps on the phone. Browser sandboxing forbids it. Web analytics can't peek into installed apps; QR analytics inherit the same limit.
Pre-scan anything. The phone doesn't tell your server how long the visitor stared at the QR before scanning, whether they tried twice, whether they hesitated. The first time your server hears about the scan is the moment the redirect URL is fetched. Everything before that is on the phone, off-network.
QR analytics vs web analytics — the overlap
The two layers measure different but overlapping things. Knowing where they meet is half the battle.
Web analytics on the destination page (GA4, Plausible, Fathom, et al.) record the same kind of header-level data the redirect did — timestamp, IP-rough geo, user-agent — plus everything that happens after the page paints: scroll depth, time on page, clicks, form submissions, conversion events. Scan analytics record the entry: which campaign, which surface, which tagged source.
The overlap matters because it lets you sanity-check one against the other. If your redirect log shows 1,200 scans in a day and your destination web analytics show 320 sessions from that source, something is eating the visitors between hop two and hop four — a slow destination, a broken landing page, a tracker that misses sessions, or a chunk of bot traffic in the scan column. The gap is a diagnostic.
The dataset overlap also means you can stop double-counting. The redirect log is the most reliable record of "how many times this campaign was hit", because it sits before any client-side tracker that might be blocked. Web analytics on the destination give you the post-arrival behaviour. Combining them is straightforward when the click id travels through (which it does on any decent platform). Keeping them separate is the mistake — half your analytics live in one tool, half in another, and the join never happens.
The static-vs-dynamic split, restated
A static QR encodes the destination URL directly into the image. The phone decodes it and navigates straight there. Your redirect server is never involved. Your scan analytics are zero. The only data you have is whatever the destination's own web analytics capture, which means you can't tell "scan" from "any other link click" downstream.
A dynamic QR encodes a redirect URL — usually a short link on a domain you control. The phone decodes the short URL, your server answers, the click is logged, and the 302 sends the visitor onward. The destination experience is identical; the visibility difference is everything.
Two reasons dynamic is the default for anyone serious about measurement, beyond the analytics gap. First, you can change the destination after the print run. If the spring landing page becomes the summer landing page, you swap the redirect target and the printed QR still works. Second, you carry campaign metadata as URL parameters, not as physical print variations. Why every QR should be dynamic by default — and the wider trade-off matrix is in static vs dynamic QR codes.
The mid-funnel implication: a campaign budgeted on static QR codes is a campaign you can't measure or fix. That's why the same pre-flight that catches a low QR code scanability score also catches "did anyone put a redirect layer behind this thing".
Interactive — what gets captured by scenario
Pick a scenario and see which fields each setup captures.
What does each setup capture?
Pick a setup above
The static row is the punchline — almost nothing comes back. Every other row gets the same HTTP envelope; the difference is what you packed into the URL.
A QR code is not a tracker. The URL behind it is. Print a code that points at a redirect on your own domain and the analytics follow; print one that points straight at a destination and the scan is invisible from the moment the camera decodes it.
What this means for privacy compliance
Most jurisdictions class redirect-side scan logs as first-party measurement of your own marketing, not as user profiling. That's lawful under GDPR's legitimate-interest basis and outside the scope of CCPA's "sale of personal data" definition, with one important caveat: an IP address is personal data in the EU, so storing raw IPs indefinitely raises retention questions even when the basis is legitimate interest.
Three patterns that keep the privacy posture clean. Hash or truncate IPs before storage — keep the geo-derived country/region/city, drop the raw octet. Set a retention window — 13 to 24 months is the conventional ceiling for analytics data under GDPR's storage-limitation principle. Disclose the measurement — a one-liner in your privacy policy noting that scans of your QR codes are logged for campaign analytics is enough for most regimes; you don't need a cookie banner because no cookie is set on the redirect.
Where the line shifts: the moment you join a scan to an identified person (signup email, customer record), you're processing personal data on a new basis and need either consent or a contractual purpose. The conversion tracking with QR codes and short links piece covers what that join looks like in practice — the scan stays anonymous; the conversion is where identity enters.
Test the tracking before the print run. Generate a QR pointing at a dynamic short link, scan it from your phone, watch the row appear in the dashboard. The free QR code generator lets you build a tracked code in under a minute and verify the redirect logs what you expect.
Open the generatorThe bot-traffic asterisk
A scan log that hasn't been filtered for crawlers will overcount. When a printed QR's destination URL gets shared as a link inside Slack, Twitter, LinkedIn, iMessage with rich previews, the platform's preview crawler fetches the URL to render a card. That preview fetch hits your redirect server and looks identical to a real scan if you're not filtering by user-agent.
The fix is the same as for any short-link platform: maintain a list of known crawler user-agent substrings (Twitterbot, Slackbot, LinkedInBot, facebookexternalhit, Discordbot, WhatsApp) and bucket them separately. The real-time link analytics piece covers why this filtering is the difference between a feed that means something and a feed that flashes when nobody scanned. For a QR campaign specifically, preview fetches usually arrive in clusters at the moment a teammate pastes the destination URL into a chat tool — a tell-tale pattern your filter should catch and quietly demote out of the campaign totals. The same filtering matters on live-broadcast scan tracking — QR codes on a Twitch overlay attract clip-aggregator and chat-preview crawlers within seconds of the URL hitting chat, and a feed that counts them as scans will tell you the subathon was a hit before any human picked up a phone.
The five-minute sanity test
Before printing anything that depends on tracking, run this:
- Generate the QR pointing at your dynamic short link. Not the destination directly — the redirect. The free QR code generator builds the image around the tracked URL in one step, so the redirect-vs-destination decision happens before you ever export a PNG.
- Scan it from your own phone over cellular (not your home WiFi — you want to see what a real visitor's geo looks like, not your home IP).
- Check the dashboard within ten seconds. The click row should be there. Country and region should match where you are. User agent should say iOS/Safari or Android/Chrome.
- Forward the short URL to a teammate and ask them to scan it. A second row appears, different IP, different geo. The campaign has two scans from two locations.
- Add a UTM bundle to the link (
?utm_source=test&utm_medium=qr&utm_campaign=preflight), regenerate the QR, scan again. The new row carries the UTMs; the older rows don't.
If any of those four steps fail, the print run isn't ready. The five-minute test catches a misconfigured redirect, a missing geo database, a stripped UTM bundle, or a tracking layer that records scans but doesn't surface them in the dashboard. Fixing it before the print run is free; fixing it after costs the print run.
Does the QR image itself contain any tracking?
No. A QR code is just a 2D barcode that encodes a URL or some text. The tracking happens at the URL the QR points at — specifically, when that URL is a redirect on a server you control. Two visually identical QR codes can have wildly different analytics depending on what's behind the URL.
Can I track scans of a QR I've already printed?
Only if the printed QR points at a dynamic short link on a domain you control. If the QR encodes the destination URL directly (a static QR), there's no way to start tracking it retroactively — the camera bypasses any tracking layer you might add later. Plan dynamic from the start, even if you don't need analytics yet.
Is the geo accurate enough to tell which city someone scanned from?
Country yes (~99%), region usually, city often but not always. Mobile carriers route traffic through regional egress points, so a scan from a suburb 30 km from a major city frequently geolocates to the city centre. Useful for "Toronto vs Vancouver" comparisons, unreliable for "downtown vs midtown".
Does the scan log capture which app was used to scan?
No — at least not for printed QR codes. The phone's camera or scanner app decodes the URL locally and hands it to the default browser, which fires the actual HTTP request. Your server sees the browser's user-agent, never the scanner's. In-app scanners that open the link in the app's own webview are the rare exception.
What's the difference between scan tracking and the analytics on my destination page?
Scan tracking is the entry record — campaign, slug, source, timestamp — captured at your redirect. Web analytics on the destination page record what the visitor does after they land — pageviews, scroll, conversion. Both layers share timestamp and rough geo; they diverge at "entry tagging" (scan side) and "post-arrival behaviour" (web side). Use them together.
Do I need a cookie banner because of QR scan tracking?
In most jurisdictions, no — the redirect doesn't set a cookie. You're logging HTTP request headers your server already sees on every connection. A short note in your privacy policy that you log scans for campaign analytics is the typical disclosure. The cookie-banner requirement kicks in if the destination page sets analytics cookies, which is a separate question from the scan log itself.
Why do my scan totals not match my Google Analytics numbers?
Three usual reasons. First, your redirect log catches preview crawlers (Slackbot, Twitterbot) that GA filters out, so scans look higher. Second, GA on the destination requires JavaScript to run; ad blockers and Safari's tracking prevention drop 10-25% of those page hits, so GA looks lower. Third, you may have a redirect chain that drops query parameters before the destination, so the GA session has no UTM tag to associate. The redirect log is closer to ground truth; the GA number is closer to "tracked sessions". Reconcile the two by joining on click id.
Sourcesshow citations
- European Data Protection Board — Guidelines on the use of location data and contact tracing tools (privacy of IP addresses and analytics)
- California Office of the Attorney General — CCPA regulations text
- GDPR — Article 6 lawful basis for processing
- MaxMind GeoIP2 accuracy documentation
- Apple — About Camera app QR scanning behaviour (iOS)
- Google — Scan QR codes with Google Lens (Android)
- MDN — HTTP Referer header reference
Try it on your own domain
Branded short links and dynamic QR codes, on your subdomain or your own domain. One-time purchase, no per-click fees.