QR codes for campaign tracking and print attribution
This is not a campaign builder
There is no single screen that plans a campaign, assigns variants and reports back automatically. What exists today, and what this page describes, is doing the job with the bulk generator (one dynamic code per variant, each carrying its own UTM tags) plus a folder in the dashboard to keep the variants of one campaign together. That is a real workflow, not a placeholder for one.
The test is the same one that decides asset tagging: does each printed piece need a different destination, or the same destination with a different label attached to it? A single flyer run with one call to action is one code. The moment you print more than one creative, more than one placement, or more than one channel for the same offer, you need a code per variant, because a shared code cannot tell you which piece of paper someone was holding when they scanned.
The scan count in OpenQR tells you the code was reached. It does not tell you what the person did after landing, and it should not have to: that is your website's job, provided the destination says which variant sent them. Each variant's URL should carry its own utm_source, utm_medium, utm_campaign and utm_content, so the same visit shows up correctly split in Google Analytics, Matomo, or whatever you already run.
A real pair of destinations for a spring sale, split across two leaflet variants, looks like this:
Leaflet A: https://example.com/spring-sale?utm_source=leaflet&utm_medium=print&utm_campaign=spring-sale&utm_content=variant-a
Leaflet B: https://example.com/spring-sale?utm_source=leaflet&utm_medium=print&utm_campaign=spring-sale&utm_content=variant-b
Bus stop: https://example.com/spring-sale?utm_source=bus-stop&utm_medium=print&utm_campaign=spring-sale&utm_content=posterKeep utm_campaign identical across every variant of one campaign and vary only utm_content (or utm_source when the channel itself differs). That is what lets your analytics roll all the variants up into one campaign report while still breaking them apart when you need to. Paste each variant's tagged URL into the bulk generator's paste mode with a label per row (“Leaflet A”, “Bus Stop Poster”), generate the batch as dynamic codes, and put them in one folder in the dashboard so the campaign stays together as its own group rather than scattered through everything else you have printed.
Every code, on every plan, shows a headline scan count, a daily series and a country breakdown over a 7-day window. That is enough to answer “did anyone scan this at all” and “is it still getting scanned a week in”. It is not enough to answer “which time of day worked” or “did people come back”, because that needs dimensions Free does not expose.
| Free | Pro |
|---|---|
| Headline scan count | Everything in Free, plus: |
| Daily series | Device, referrer, region and town/city |
| Country | Connection class (mobile vs datacenter, a bot-traffic tell) |
| 7-day rolling window | Weekday x hour day-part heatmap |
| Full history, not just 7 days |
The heatmap is the actual answer to "which variant worked"
A bare scan count tells you a variant got attention. The day-part heatmap and first-scan-vs-repeat pattern tell you whether that attention clustered around when the leaflet actually landed on doormats, or a poster's commute-hour placement, which is the difference between a creative that worked and a creative that just happened to be near more people. That comparison is a Pro feature (`detailedAnalytics`), because it is the dimension that turns a scan count into a decision.
Static codes are free, unlimited and never expire, but a static code cannot be repointed and carries no scan count, so it is not useful for attribution once the campaign is live. Dynamic codes are the ones that give you a scan log per variant, and the free plan includes three. A two-variant leaflet test fits inside that. A leaflet, a bus stop poster and a mail drop already uses all three. Anything past that, including simply re-running last season's variants alongside this season's, needs Pro (£9/month), and it is worth deciding that before the print order goes in rather than discovering it when the fourth code will not save.
Running a print attribution test, start to finish
Write one destination URL per variant
Same landing page, same utm_campaign, a different utm_content or utm_source per variant so your own analytics can split them.
Generate one dynamic code per variant
Paste the labelled list into the bulk generator; a shared code across variants is the one mistake that makes the whole test unreadable.
Group them in one folder
So the campaign's codes stay together in the dashboard instead of mixed in with everything else you've printed.
Print, place, and note what changed
Placement and timing move scan counts as much as the creative does; write down what else was different so you don't credit the wrong variable.
Read scans against placement, not against ambition
A code with few scans may mean a weak creative, or it may mean it went somewhere fewer people walked past. Check placement before you retire a variant.
Export or pull the numbers once the run has had time to land
CSV from the dashboard for a one-off report, or the API for anything you'll want to repeat next campaign.
A scan means somebody pointed a phone at that specific piece of paper and it decoded. It does not mean they read the flyer, liked the offer, or bought anything: that part of the funnel is your landing page's and your analytics' job, which is exactly what the UTM tag is for. And a variant with almost no scans is not automatically a weak creative. It might be a strong creative in a place nobody walks past. Before writing off a variant, check where it went as well as what it said.
No scanner is ever identified, and that's what makes this shareable
Scan data is aggregated as it is written: there is no per-scan event to re-identify, no cookie, no device ID, nothing linking two scans to one person. Geography is never named below five scans in a window (k-anonymity, k=5), so a city only appears once enough people in it have scanned. That is what lets you hand a campaign report to a client or a colleague without a privacy conversation first, and it's the same reason OpenQR runs with no consent banner: there is no personal data being collected to consent to.
One client with three variants is a five-minute job in the dashboard. Ten clients, each with their own seasonal campaigns, is not something you want to click through by hand. Every number above (scans, the daily series, and the detailed breakdowns on Pro) is available as a CSV export from the dashboard for a one-off report, and through the REST API and MCP server for anything you want to pull automatically into a client dashboard or a reporting script. The API is not paywalled behind Pro: the plan boundary is on the analytics detail returned, not on whether you can reach it at all, so a Free-plan integration gets exactly the same headline numbers a Free-plan dashboard user sees.
Make the whole set at once
The bulk tool opens seeded with the shape of this job: example rows in your vocabulary, and the filename pattern that suits it. Free, unlimited and with no account for static codes.
Open the bulk generator