13 October 2026 · 9 min read

GA4 Promotions and Coupons: Measuring What Actually Sells

GA4 has two things that both get called a promotion, and they almost never meet. One is an internal banner or tile you use to push something on your own site. The other is a discount code a customer types at checkout. Different events carry them, different reports read them, and a team that treats them as a single job ends up with a Promotions report full of (not set) and a coupon breakdown that shows nothing at all.

Two systems, one word

An internal promotion advertises one part of your site from another: a hero banner, a promotional tile, a seasonal strip above the navigation. It need not involve a product at all, which is why GA4 gives it its own two events. view_promotion records that a promotion was shown. select_promotion records that it was clicked.

A coupon is a discount code. It travels on the purchase event, and optionally on each item inside that event. There is no coupon event, and there is no coupon report separate from the purchase data.

Those two paths feed different reports. The Promotions report under Monetization reads only the promotion events. The Purchase revenue by Order coupon card in the Monetization overview reads only the purchase event. Sending one correctly does nothing for the other.

The promotion events, and the one optional parameter that is not optional

Google's ecommerce guide describes the impression as part of the initial screen view and shows view_promotion carrying promotion_id, promotion_name, creative_name, creative_slot and an items array. The same shape applies to select_promotion on the click. All of those parameters appear as optional in the event reference.

There is a note underneath them, and it is the whole game: if the items array is not included, some reports, such as the Promotions report, and metrics will not include this promotion event. Leave items out and the event still shows up in Realtime and in the All events table, so the implementation looks healthy while the report you built it for stays empty.

Decide what sits in the items array before you build. If a banner promotes one product, send that product. If it promotes a sale with no single product behind it, send an item that describes the promotion itself, with an item_id like promo_summer_sale and a readable name. Both work. Sending nothing does not.

Item scope, event scope, and the order GA4 checks them

The event reference is explicit about how GA4 resolves a promotion parameter. For each of promotion_id, promotion_name, creative_name and creative_slot, Analytics first looks inside each object in the items array. If the value is not there, it falls back to the event-level parameter. An item-scoped promotion dimension reports (not set) only when the parameter is missing from both places.

That gives you two valid patterns and one invalid one. Put the promotion at the event level when a single promotion covers the whole batch, the common case for a site-wide banner. Put it inside the item when different products in the same event carry different promotions. Mixing the two without deciding is the failure: some events then resolve at item scope, others at event scope, and the Item promotion name dimension holds a mixture of both plus (not set).

What the Promotions report shows

Find it at Reports, then Life cycle, then Monetization, then Promotions, or under Drive online sales in the Business objectives collection. If it is missing from the navigation, an editor or administrator can add it back.

The dimensions are all item-scoped: Item promotion name, Item promotion ID, Item promotion creative name, and Item list position, which comes from the item-level index parameter. Item promotion creative name is fed by creative_name and lets you compare two placements of the same promotion.

The metrics map onto events. Items viewed in promotion comes from view_promotion, items clicked in promotion from select_promotion. Items added to cart comes from add_to_cart and items checked out from begin_checkout, so a promotion can be followed down the funnel rather than stopping at the click. Items purchased and item revenue come from purchase, and item revenue is price multiplied by quantity, excluding tax and shipping.

Item promotion click-through rate is worth reading carefully. Google defines it as the number of users who selected a promotion divided by the number of users who viewed the same promotion. It is a user ratio, not an event ratio, so somebody who sees the banner five times and clicks once counts as one view and one click.

The order coupon is not the item coupon

The coupon parameter is defined twice in the event reference, and the two definitions do not interact. At the event level, coupon is the coupon name or code associated with the event. Inside an item, coupon is the coupon name or code associated with that item. The reference states the conclusion plainly: event-level and item-level coupon parameters are independent.

In practice that means one decision. If a single code applied to the whole basket, send it at the event level and read it in the Purchase revenue by Order coupon card. If different lines of the same order used different codes, the order card cannot express that, and you need the item-level coupon so revenue can be split line by line.

Whatever you choose, standardise the spelling. GA4 stores the code as a string with no normalisation, so SUMMER_FUN, summer_fun and SUMMERFUN become three separate rows with three separate revenue figures. A short, agreed code list is worth more here than any tagging trick.

Discounts change what price means

discount is a unit discount, not a line discount. The reference describes it as the unit monetary discount value associated with the item, and the item revenue formula is price multiplied by quantity.

The instruction that matters is this one: if a discount applies to the item, set price to the discounted unit price and specify the unit discount in the discount parameter. The two halves have to stay consistent. Send the original price with a discount attached and the discount figure double counts. Send the discounted price with no discount and every discount figure reads zero while the revenue line looks correct. Neither failure produces an error message anywhere.

Refunds, and the number at the top

A refund is its own event and replaces the older ecommerce_refund. The transaction_id is required on purchases and refunds, and the refund carries the original one, which is what makes the two net out.

Google recommends including item information in the refund event so item-level refund metrics work. Skip the items and you still get the event-level correction, but nothing at line level. Total revenue is purchase revenue plus advertising revenue minus refunds, so an unsent refund keeps the top line high and every trend underneath it inherits the error.

The parameter rules that decide whether anything populates

Two help pages decide this, and both are short. The ecommerce purchases report page states that an event missing required parameters is treated as a custom event and will not appear in the report. The fix missing revenue data page separates the calculations: event-level revenue metrics such as Total revenue use the event-level value and currency parameters, while item-level metrics such as Item revenue use price and currency.

So on a purchase event you need currency, transaction_id, value and items at the event level, and an item_id or item_name inside each item. For item revenue you also need price and quantity, with quantity defaulting to 1 if you leave it out.

Three formatting rules catch most broken implementations. value has to be a number and not a string, so no quotation marks, no currency symbol, and the currency belongs in the currency parameter as a three-letter ISO 4217 code. currency has to be a string, not a number. And both value and price are lower case and spelled exactly, because GA4 will not guess what you meant.

There is room for up to 27 custom parameters inside each item, but none appear in reports until you register a custom dimension or metric and add it to the report.

Implementation discipline for the promotion events

Four habits separate a promotion report that gets used from one that gets abandoned.

The first is to record only what somebody could actually see. Firing view_promotion for every banner in the markup at page load creates impressions nobody had and inflates the denominator of the click-through rate. Track visibility in the viewport, or restrict it to what is above the fold.

The second is to keep IDs stable and names readable. promotion_id is the join key for BigQuery exports, advertising reports and later reconciliation. promotion_name is what a human reads. Changing a name mid-campaign splits one promotion into two rows with no way to merge them.

The third is to use creative_name and creative_slot deliberately. The slot is the placement, the creative is the asset. Reuse the slot labels across campaigns or the cross-campaign comparison stops meaning anything.

The fourth is to test in the right order. DebugView first, because it shows the parameters as GA4 received them. Realtime second, to confirm the event reaches the property rather than only the debug stream, and the Promotions report last, because a fix does not backfill the hours before it.

Consent, UK rules, and personal discount codes

Promotion and coupon tracking runs on analytics consent like everything else. A visitor who declines produces no view_promotion at all, so the Promotions report is a consented subset of reality. State that in the report rather than let someone discover it later.

The ICO's guidance on storage and access technologies is the reference for the UK. Analytics storage is not strictly necessary, so it needs consent, and advertising storage cannot be folded into an analytics permission. Nothing loaded before the consent default should be setting identifiers on the basis of a promotion click.

One case deserves attention. A personalised discount code is personal data, and if it lands in promotion_id it travels into the event, into every export and into BigQuery. Keep promotion_id about the campaign. Let the order system own the individual code. If the marketing team wants that individual code tracked, treat the parameter as personal data and handle the export on that basis.

A short routine before the next campaign

None of this needs a new tool or a bigger budget. It needs the promotion to arrive as an event with items attached, the coupon to sit at the level the question asks about, and the price to agree with the discount. Teams that get those right can answer the only question a promotion report exists for: which of this activity sold something, and which took up space.

Can You Tell Which Promotion Sold Anything?

North Digital sets up promotion and coupon tracking so the banners, the codes and the discounts all report as separate, usable numbers, and the funnel behind them holds together.

Talk to North Digital