QR codes for campaign tracking and print attribution

A stack of freshly cut blank leaflets beside the blade of a paper guillotine.

QR codes for campaign tracking and print attribution

A print run does not tell you what worked. Two thousand leaflets go out with a headline and a photo, a bus stop poster runs the same offer in a different format, and a month later someone asks which one paid for itself. Without a code that is unique to each printed piece, the honest answer is a shrug. Give every variant its own dynamic QR code and the question stops being a guess: each one has its own scan count, its own timing, and its own UTM tag showing up in your own analytics, not just ours.

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 job is one code per variant, not one code reprinted everywhere

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.

  • Two headlines on the same leaflet, split between two print runs (an A/B test on paper)
  • The same offer on a leaflet, a bus stop poster and a shop window sticker (a channel test)
  • A direct mail drop repeated across three postcodes with the same creative
  • A trade show handout compared against the same offer mailed to the same list
  • A seasonal campaign re-run months later, kept as its own variant so it does not blend into last time's numbers

UTM parameters carry the attribution into your own analytics too

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=poster

Keep 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.

Comparing variants side by side, and what plan that needs

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.

FreePro
Headline scan countEverything in Free, plus:
Daily seriesDevice, referrer, region and town/city
CountryConnection class (mobile vs datacenter, a bot-traffic tell)
7-day rolling windowWeekday 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.

A campaign with more than three variants needs Pro, so decide that up front

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

  1. 1

    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.

  2. 2

    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.

  3. 3

    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.

  4. 4

    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.

  5. 5

    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.

  6. 6

    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.

What a scan does not prove

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.

Running this at agency or multi-client scale

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.

Yes. Generate a separate dynamic QR code for each design, each pointing at the same landing page with a different utm_content tag, print them on their respective runs, and compare the scan counts once both have been in circulation for the same length of time.

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