Analytics

How revenue is attributed to campaigns

Exactly how the app decides which order lines count as campaign revenue, what it records, and why the numbers are conservative by design.

Your Analytics figures are built from one simple rule: a line on an order is either tagged to a campaign or it is not. There is no last-click guesswork, no time-decay, and no fuzzy modeling. This page explains the tagging mechanism, the (effectively non-existent) attribution window, how the recorded amount is calculated for orders placed and for post-purchase upsells, and every edge case that changes the number you see.

The tagging mechanism

When a shopper accepts an offer and the offered product is added to the cart, the app writes a hidden line-item property onto that cart line. The property name is _<appName> and its value is the campaign ID that produced the offer. (The underscore prefix keeps the property hidden, so shoppers never see it on the cart, at checkout, or on their order confirmation.)

When the order is placed, Shopify sends the app an orders/create webhook. The app walks every line item on the order and looks for that exact property name. Only lines that carry the marker are attributed to a campaign; every other line — products the shopper found and added on their own — is ignored for campaign revenue.

This makes attribution line-item-level and exact:

  • A line is matched to a campaign by an explicit marker that the app itself wrote when the offer was accepted.
  • The campaign that gets credit is the one named in the marker's value — not "the most recent click" or "whatever campaign was live."
  • If a line has no marker, it contributes zero to campaign sales, even if it is the very same product an offer was promoting.

The marker lives on the cart line, not on the shopper, the session, or a cookie. That is what makes the rest of this page work: attribution travels with the line item itself, all the way to checkout.

The attribution window

There is effectively no lookback window.

Attribution is decided at the moment the order is placed, by reading the marker present on the cart line at that instant. The app does not ask "when was this offer accepted?" or "was the campaign still running at checkout?" It only asks "does this line carry a marker?"

The practical consequences:

  • An offer accepted days earlier still attributes, as long as the tagged line survives in the cart until checkout.
  • It does not matter if the campaign was paused, edited, or even deleted between acceptance and checkout — the recorded marker value is what gets credited. (Deleted campaigns still keep their data; see the (Deleted campaigns) filter on the Analytics page.)
  • Conversely, if the tagged line leaves the cart before checkout, there is nothing to attribute — there is no "recent acceptance" memory to fall back on.

If you are used to marketing attribution windows — for example "credit a click for 7 days" — this model will feel different. There is no decay curve and no expiry. The question is binary and answered at checkout: is the tagged line still in the cart? Yes means full credit to the campaign in the marker; no means no credit at all.

How at-placement revenue is computed

For orders that come through orders/create, the app computes each tagged line's value after discounts — the amount the shopper actually paid for the offered line.

For every tagged line:

StepFieldCalculation
1Original line totalprice × quantity
2Discounts on the lineSum of Shopify's discount_allocations for that line
3Final line totalOriginal line total − total discounts
4Per-item priceFinal line total ÷ quantity, rounded to 2 decimals

The Final line total is what the app records as that campaign's contribution to the order — it becomes the campaign's totalPrice, which surfaces as Campaign Amount in the orders list and rolls up into Total sales. The per-item price is stored alongside the line's quantity and variant.

Because step 2 subtracts every Shopify discount allocated to the line — automatic discounts, discount codes, and any campaign discount the app itself applied — at-placement revenue reflects the net price, not the list price.

Discounts are read from Shopify's own discount_allocations on the line, so the subtracted amount matches exactly what Shopify attributed to that line. The app does not re-derive the discount itself.

Worked example

A shopper accepts an offer for a product priced at $20.00, quantity 2, and a 10% campaign discount allocates $4.00 to the line:

  • Original line total: 20.00 × 2 = 40.00
  • Discounts on the line: 4.00
  • Final line total (recorded as Campaign Amount): 40.00 − 4.00 = 36.00
  • Per-item price (stored): 36.00 ÷ 2 = 18.00

Post-purchase is different

Post-purchase offers — the one-click upsell a shopper sees on the order-status page after they have already paid — do not go through orders/create. Accepting one edits the existing order, so Shopify sends an orders/edited webhook instead, and the math is intentionally different.

When that webhook arrives, the app:

  1. Loads the order and reads its campaignId metafield (written under the app's namespace when the post-purchase offer was served). If there is no such metafield, nothing is attributed.
  2. Looks at the line-item additions in the edit and finds, for each, how many units were added (the delta).
  3. Fetches each added variant and reads its listed price (variant.price).
  4. Computes the added value as variant.price × delta for every addition and sums it.
  5. Adds that value to the same order's campaign figures (under the campaign from the metafield) and to the order's orderSubtotal.

If the order does not yet exist in the app's records — which happens when the only attributed offer on the order is the post-purchase one — the app creates the order record at this point and seeds the campaign with the added value.

Post-purchase revenue is recorded at list price and does not subtract post-purchase discounts. At-placement lines record the net, after-discount amount; post-purchase additions record variant.price × quantity added. If a post-purchase offer carried a discount, the figure the app stores can be higher than the net amount Shopify shows for that edit. Treat post-purchase Campaign Amount as a list-price figure, not a net-paid figure, when you reconcile against Shopify.

See Post-purchase campaigns for how these offers are built and served.

Multiple campaigns on one order

A single order can carry lines from several different campaigns at once — for example a pre-purchase upsell and a post-purchase upsell, or two different offers accepted in the same cart.

  • Each campaign is keyed separately inside the order's campaigns map. One campaign's revenue never bleeds into another's.
  • The Campaign Amount column in the orders list sums every campaign's contribution on that order, so a single row can represent two or more offers.
  • The order is still counted once in the orders count, no matter how many offers it contains. Orders count = orders that contain at least one offer, not the number of offers.

The campaigns map model

Under the hood, each tracked order is stored as a map from campaign ID to that campaign's contribution:

order
├── orderSubtotal        (full order value, all items)
├── currency
└── campaigns
    ├── <campaignId-A>
    │   ├── items        [ { id, quantity, price }, ... ]
    │   └── totalPrice   (sum of this campaign's lines on this order)
    └── <campaignId-B>
        ├── items        [ ... ]
        └── totalPrice

One order maps to one or more campaign entries; each entry holds the offered line items and that campaign's totalPrice. This shape is what the daily metrics roll up: Total sales sums totalPrice across campaigns, and Total sold offers sums the item quantities.

Edge cases and recipes

Why attribution is conservative by design

Every rule on this page errs toward under-counting rather than over-counting campaign revenue. Only an explicitly tagged line counts; organic purchases of the same product never do; at-placement revenue is net of discounts; and there is no speculative time-window credit. The number you see is revenue you can defend — a product the app actively offered and the shopper actually bought on that line.

The one deliberate exception is post-purchase, which records at list price and ignores post-purchase discounts. Keep that in mind when your Analytics total and your Shopify reports diverge: at-placement figures are net, post-purchase figures are gross.

Reconciling against Shopify

When your Analytics numbers do not line up exactly with a Shopify report, the difference almost always comes from one of the rules above:

  • Analytics counts only offered lines, never the whole order. Compare against line-item revenue for the tagged products, not the order total.
  • At-placement amounts are after discounts; post-purchase amounts are at list price and before post-purchase discounts.
  • Currencies are never converted. Each currency is tracked and reported separately.
  • Days are bucketed in your store's timezone, matching how Shopify groups your reports.

On this page