The two-layer architecture
- Printed QR
- VastQR redirect
- Tagged destination URL
- Your website
- GA4
An example destination:
https://example.com/menu?utm_source=offline&utm_medium=qr&utm_campaign=spring_menu&utm_content=richmond_table_14
Nobody sees that URL. The printed artwork encodes the short redirect, so the parameters can be long and descriptive without making the QR pattern denser or harder to scan.
| The question | Answered by |
|---|---|
| Did Table Tent 14 get used? | QR redirect analytics |
| Which placement produced the most scans? | QR redirect analytics |
| Did those visitors read the menu? | GA4 |
| Did the campaign produce orders? | GA4 |
| How many requests never became a page view? | The gap between the two |
What GA4 actually measures, and where it starts
Worth being blunt about, because almost every “GA4 is under-reporting my QR codes” conversation starts here: GA4 never sees the scan. It has no visibility of a camera recognising a pattern. It starts existing much later in the chain, and everything before that point is invisible to it by construction.
- Camera recognises the code
- User taps the link
- Redirect receives the request
- Browser follows it
- Page begins loading
- GA4 tag runs
A person can be recognised by the camera and never tap. They can tap and close the tab before the page loads. Consent tooling, a blocker, a dead zone or a slow page can all end the journey between box three and box six. That is why:
Decide the naming convention before you print
The most common GA4 mistake is not a missing parameter. It is three people using qr, QR and print for the same thing, and someone spending a day cleaning it up in a spreadsheet six weeks later.
Being precise about what is and is not recoverable here matters, because the two halves behave differently. A dynamic QR code’s destination stays editable after printing, and so do the UTM parameters attached to it — you can fix a convention at any point, and every scan from then on arrives under the corrected names. What no product can do is go back and rename the sessions GA4 already filed. Those months stay split across whatever names were in use at the time.
So the cost of deciding late is not a reprint. It is a gap in the record. Pick once, write it down, and apply it to every destination in the batch:
| Parameter | What it should carry | Example |
|---|---|---|
utm_source | The origin family | offline |
utm_medium | The mechanism | qr |
utm_campaign | The business campaign | fall_menu_2026 |
utm_content | The physical placement | richmond_table_14 |
utm_term | Usually unnecessary for QR | leave empty unless you have defined a use |
One consequence is worth knowing before you commit: a custom value such as utm_medium=qr may not match any of GA4’s default channel rules, so the traffic can appear under Unassigned even when Session source / medium shows your values correctly. That is a labelling problem with a proper fix, and it is covered below — it is not a reason to pick a dishonest medium.
QR UTM Campaign Builder
Build the tagged destination for one printed placement. Source and medium start on the convention above; everything stays editable, and any query parameters your URL already carries are preserved.
Enter a destination and the three required parameters to build the tagged URL.
This builds the tagged destination. It does not mean GA4 recorded a scan: a redirect request and a GA4 session measure different boundaries, and consent, ad blockers and visitors who leave before the page loads all sit between them.
Need codes whose destination you can change after printing? Create trackable dynamic QR codes with VastQR.
Setting it up, step by step
- Confirm GA4 is already working on the destination site
Open the page and check the property is receiving ordinary web activity. Do this first — otherwise you will spend an afternoon blaming QR attribution for a missing tag.
- Build the tagged destinations
One row per placement, each with the same campaign and a distinct
utm_content. A spreadsheet is the right tool here: it is easy to see at a glance whether the taxonomy was applied consistently. - Create the dynamic codes from that list
Paste or upload the CSV so each tagged URL becomes the destination of one managed code. The parameters live in the destination, not in the printed pattern.
- Scan one before printing anything
On a real phone. Confirm the redirect resolves, the final URL loads, the parameters survived the redirect, and GA4 registered the session.
Parameters surviving the redirect is the step people skip and the step that most often breaks.
- Verify traffic-source reporting in GA4
Check Traffic acquisition, or build an exploration on the session-scoped source, medium, campaign and content dimensions. Be explicit about whether you are looking at user-, session- or event-scoped dimensions; they will not agree, and that is by design.
Because a QR campaign is manually tagged, the fastest place to confirm the values is GA4’s Manual report, which exists specifically for manually tagged traffic. The dimensions to read there are named:
Session manual sourceSession manual mediumSession manual campaign nameSession manual ad content— where yourutm_contentplacement lands
Read the raw dimensions before you judge the channel label. Session source / medium can be perfectly correct while the default channel group still reads Unassigned — see the section below.
- Print a physical proof and scan that
At final size, from the distance people will actually stand. A browser test proves the tagging; only the printed proof proves the deployment.
Have your tagged destinations ready?
Paste the list, see which rows validate, and preview the whole batch before you pay for anything.
A reusable template for a QR campaign
Sixty codes across three stores, one campaign called summer_combo_2026. Every destination shares the source, medium and campaign; only the content changes:
| Placement | utm_content | Everything else |
|---|---|---|
| Richmond, table 1 | richmond_table_01 | source=offline · medium=qr · campaign=summer_combo_2026 |
| Burnaby, table 1 | burnaby_table_01 | identical |
| Vancouver, window | vancouver_window_01 | identical |
Now GA4 can report the campaign as one thing and still break it down by placement, while VastQR reports which individual code was scanned. Two systems, one taxonomy, no conflict.
Separating placements without wrecking your reports
There are two strategies, and only one of them scales.
Unique utm_content — use this by default
Right whenever the placements belong to the same campaign. The campaign stays legible and the detail is still there when you want it.
Unique campaign names — only for genuinely separate campaigns
Right when the placements have different objectives and different owners. Wrong as a way of separating three hundred table cards, which turns the campaign report into a directory.
When GA4 says Direct, or says Unassigned
These are the two reports that send people looking for a broken QR code, and they mean opposite things. One says the campaign data never arrived. The other says it arrived and Google did not recognise the category you put it in. Fixing the second the way you would fix the first will corrupt your reporting.
Which one are you looking at?
- (direct) / (none)The session carried no usable campaign or referral information. The tags are missing or were lost on the way.
- offline / qr, channel UnassignedThe tags arrived intact. GA4's default channel rules simply have no bucket for your medium. Nothing is broken.
- (not set) source / mediumSession or tag configuration, not the QR code. Check the property is receiving ordinary web activity at all.
Direct means the parameters did not survive
For a QR campaign, that usually has one of four causes: the printed destination never carried parameters; a redirect somewhere in the chain stripped them; the final landing URL lost them before GA4 read the page; or the session started without any attribution information to work from.
This is why the pre-print test has to inspect the final landing URL rather than simply confirm the code opens something:
- Printed QR
- Redirect
- Landing URL still carries the parameters
- GA4 records the visit
Unassigned means the tags worked and the label did not
GA4’s default channel group is a fixed set of classification rules. A medium like qr can be exactly right for your own taxonomy and still match none of them. What you see then is:
Session source / medium = offline / qr
Session default channel group = UnassignedThe attribution is correct. Only the label is missing. Diagnose in this order and you will not confuse the two cases:
- Check Session source / medium
If it reads
offline / qr, your parameters arrived. Stop suspecting the QR code. - Check the campaign dimensions
Session campaign and the manual content dimension should carry the names you defined before printing.
- Confirm the values survived the redirect
Open the short link yourself and read the address bar on the landing page.
- Only now look at the channel group
If steps one to three are correct, what you have is a classification problem, and it is fixed in reporting rather than in tagging.
Labelling QR traffic email or referral because those map neatly into a default channel is a lie you will have to keep telling. Keep the taxonomy that describes what actually happened, and build a custom channel group — something like Offline QR — with a rule matching the source and medium you really use.
A custom channel group re-categorises data that was already collected. It does not rewrite the underlying source and medium values, and it cannot recover parameters that were never captured in the first place. That asymmetry is the whole argument for deciding your naming before the print run.
| What GA4 shows | What it actually means | First thing to check |
|---|---|---|
(direct) / (none) | Campaign information missing or lost in transit | Did the final landing URL still carry the parameters? |
offline / qr + Unassigned | Tags arrived; default channel rules did not match them | Session source / medium — then leave the tags alone |
(not set) | Session or tag configuration issue | Whether the property receives ordinary web traffic |
| Right source, wrong grouping | A reporting taxonomy problem | Custom or primary channel-group rules |
Why redirect counts and GA4 sessions never match
Between a scan and a GA4 session there are seven things that have to happen: the camera recognises the code, the person taps the link, the redirect receives the request, the browser follows it, the destination begins loading, the GA4 tag executes, and consent and browser settings permit measurement. Any of them can fail independently.
Divergence is therefore expected. What matters is that each system is internally truthful about what it measured.
| What happened | QR redirect count | GA4 session |
|---|---|---|
| Person scans, page loads normally | Yes | Likely yes |
| Person opens, then closes before the page loads | Yes | Often no |
| A link preview or crawler requests the short link | Yes, labelled where recognised | Usually no |
| Page loads but consent is denied or the tag is blocked | Yes | Often no |
| Visitor refreshes the landing page | No new request | More GA4 activity |
| Same person scans three times | Three events | Depends on session timing |
Automation is a large part of that gap. VastQR counts recognised automated requests in its default All events view and reports the classified share, and offers an Exclude classified automated view when you want the period recalculated without them. Neither view is a verified-person count — the classifier only sees what announces itself — but having the figure in front of you is what makes the difference between the two systems explainable.
Do not demand equality between the two. Demand that you can explain the gap.
What happens to attribution when you edit a live destination
A dynamic code lets you change the destination after printing, which creates a reporting decision most guides skip.
- Same campaign, page moved: keep the taxonomy identical. Changing it splits one campaign into two for no analytical reason.
- New campaign entirely: update the parameters along with the destination, because the old campaign name no longer describes what the visitor is seeing.
Either way, write down the date. The parameter change applies to traffic from that moment on; the sessions GA4 already recorded keep the names they were filed under. A before-and-after comparison that silently spans both is worse than no comparison.
GA4 does not make a static code dynamic
You can put a tagged URL inside a static QR code and measure the resulting website traffic in GA4. That is genuinely useful, and it is why the claim “static QR codes cannot be tracked” is wrong.
What GA4 does not give the static code:
- an editable destination;
- a managed redirect;
- a pause state;
- a library of codes to manage;
- a scan count for the code itself, as opposed to a session count for the page.
That is the exact line between website attribution and dynamic QR operations.
Frequently asked questions
Do I need GA4 to track QR scans?
Can a static QR code use GA4?
Why are my QR scans higher than my GA4 sessions?
Why does my QR traffic show up as Direct in GA4?
Why does utm_medium=qr sometimes show as Unassigned?
Should every QR code get a unique UTM?
Does VastQR integrate with GA4 automatically?
Should utm_source be qr or offline?
Sources
- Google Analytics Help — URL builders: collect campaign data with custom URLs
- Google Analytics Help — Traffic-source dimensions, manual tagging and auto-tagging
- Google Analytics Help — the Manual report for manually tagged campaigns
- Google Analytics Help — Analytics dimensions and metrics
- Google Analytics Help — Understand (direct) / (none) traffic
- Google Analytics Help — Default channel group
- Google Analytics Help — Tagging best practices to avoid unassigned, (not set), and direct traffic
- Google Analytics Help — Custom channel groups