# Campaign attribution

Source: https://gokarla.io/docs/guides/tracking-page/attribution

# Campaign attribution

Attribution answers one question: **which orders did your tracking-page
campaigns generate?** Karla answers it by tagging every campaign link with
query parameters, capturing those parameters in your shop when the customer
buys, and matching the result back to the campaign in your analytics.

## How attribution works

1. A campaign (main, product, or banner promotion) is shown on your tracking
   page, in a notification email, or on the thank-you page.
2. When the customer clicks the call-to-action, Karla has already appended its
   attribution parameters to the target URL — no setup needed on the link
   side.
3. Your storefront **captures** the parameters. This is the only step where
   you have choices to make — see [capture paths](#capture-paths).
4. When the order is created, the captured parameters are stored on the Karla
   order as `order_analytics`.
5. Karla matches the `campaign` value back to the campaign and reports
   impressions, clicks, orders, and revenue in the
   [portal analytics](#where-attribution-shows-up).

```mermaid
flowchart LR
    A["Campaign shown<br/>(tracking page, email,<br/>thank-you page)"]
    B["Customer clicks CTA<br/>URL carries karla_* params"]
    C["Storefront captures params<br/>(pixel, SDK, plugin,<br/>cart attributes, or API)"]
    D["Order created<br/>stored as order_analytics"]
    E["Campaign matched<br/>reported in Analytics"]
    A --> B --> C --> D --> E
```

Discount codes are an independent, complementary signal that skips steps 2–3
entirely — see [discount code attribution](#discount-code-attribution).

## The attribution parameters

Every campaign call-to-action link carries these query parameters:

| Parameter        | Example value                          | Meaning                                                                                            |
| ---------------- | -------------------------------------- | -------------------------------------------------------------------------------------------------- |
| `ref`            | `karla`                                | Marker present on every Karla-originated link. See [source markers](#source-markers) for variants. |
| `karla_source`   | `trackpages`                           | The surface the click came from.                                                                   |
| `karla_medium`   | `product_promotion`                    | The promotion type: `basic_promotion`, `product_promotion`, or `banner_promotion`.                 |
| `karla_campaign` | `9f1c6a7e-3b2d-4e8f-9a10-1234567890ab` | The campaign's unique ID — this is what links the order to the exact campaign.                     |

Product-promotion links that are rendered on a specific order's tracking page
additionally carry the **originating order**, so you can trace a repeat
purchase back to the order whose tracking page produced it:

| Parameter                 | Meaning                                |
| ------------------------- | -------------------------------------- |
| `karla_order_external_id` | Platform ID of the originating order.  |
| `karla_order_number`      | Order number of the originating order. |
| `karla_order_name`        | Display name of the originating order. |

Two things worth knowing:

- **Karla never overrides existing parameters.** If a target URL you
  configured already contains `karla_source` (or any other parameter), your
  value wins.
- **The parameters describe the campaign, never the shopper.** They contain
  no user data, which keeps the privacy story simple — see the consent notes
  under each capture path.

### Source markers

The `ref` parameter distinguishes which Karla surface produced the click:

| Marker               | Set on                                                                                                       | Captured by                                                                                         |
| -------------------- | ------------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------- |
| `ref=karla`          | All campaign links on tracking pages and in notifications.                                                   | All capture paths.                                                                                  |
| `ref=karla-thankyou` | Call-to-action links of the [thank-you page promotion](/docs/guides/shops/shopify) in the Karla Shopify app. | Browser SDK global mode. The Shopify pixel records the `karla_*` parameters those links also carry. |
| `ref=karla-lounge`   | Links from the Karla Lounge brand-deals page.                                                                | Browser SDK global mode (also as legacy `source=karla-lounge`).                                     |

## Capture paths

The parameters only matter if something in your storefront records them and
attaches them to the order. All five paths below produce the same result — an
`order_analytics` record on the Karla order. **On Shopify with the Karla app
installed, the app pixel is the default choice**; the other paths cover the
stacks where the pixel can't capture — headless storefronts, Shopware, and
fully custom platforms. Paths can coexist; the first captured attribution
wins. Each path also implies its own
[attribution window](#attribution-windows) — how long a captured click can
still be matched to an order.

### Shopify app pixel

The Karla Shopify app ships a
[web pixel](https://shopify.dev/docs/apps/build/marketing-analytics/build-web-pixels)
that captures the parameters on landing and sends them to Karla only when a
checkout completes.

- Enable it once in the Karla app under **Settings → Campaign Attribution**
  (pixel toggle, consent requirement, optional browse tracking).
- The pixel stores the parameters in **session storage** (key
  `karla_attribution`) — scoped to the current tab, first click wins within
  the session. The stored attribution is cleared when the tab's session ends
  or when a checkout completes (it is sent to Karla, then cleared).
  Alongside the `karla_*` parameters it records `ref` only when the value is
  exactly `karla`.
- **Consent** is yours to configure: **No consent required** (default — the
  parameters are non-PII and short-lived), **Analytics consent required**, or
  **Marketing consent required**. With a consent level set, the pixel stays
  idle until your privacy banner reports that consent.
- **Browse tracking** (beta) buffers viewed product IDs in local storage (key
  `karla_browse`) and always requires **analytics consent**, independent of
  the attribution setting. The buffer is purged as soon as an order
  completes, consent is withdrawn, or the feature is turned off.

### Browser SDK global mode

If you embed the tracking page with the [Browser SDK](/docs/platform/browser-sdk),
you can load it site-wide in `global` mode. With the Karla Shopify app
installed, the [app pixel](#shopify-app-pixel) already covers capture —
global mode mainly serves Shopify storefronts running without the app:

```html
<script
  id="karla-bundle"
  src="https://browser.gokarla.io/latest/bundle.min.js"
  data-shop-slug="your-shop-slug"
  data-starter-page="global"
></script>
```

On every page load it checks the URL for attribution parameters and writes
them as **cart attributes** via Shopify's `/cart/update.js`:
`_karla_source`, `_karla_campaign`, `_karla_medium`, `_karla_captured_at`,
`_karla_landing_url`, `_karla_landing_path`, `_karla_referrer`,
`_karla_version`. Shopify copies cart attributes onto the order, where Karla
reads them back into `order_analytics`.

- **First click wins per cart** — once `_karla_source` is on the cart, later
  visits don't overwrite it.
- **The attribution lives exactly as long as the cart.** It ends when the
  checkout completes (the attributes move onto that order), the cart is
  emptied, or the cart cookie expires — typically around 14 days after the
  last cart activity (Shopify doesn't document the exact lifetime).
- Accepts `karla_source` or the legacy markers `ref`/`source` equal to
  `karla` or `karla-lounge`.
- The landing URL is stripped of sensitive query parameters (tokens, emails,
  click IDs, and ~40 more) and truncated to Shopify's 255-character
  attribute limit. Nothing is written to cookies or browser storage — the
  data lives on the cart itself.
- Shopify-only: the mode deactivates quietly when `/cart.js` doesn't exist.
  This includes headless Shopify storefronts — see the next path.

### Headless Shopify cart attributes

On a headless Shopify storefront (Hydrogen, Next.js, or any custom frontend
on the Storefront API) the two automatic paths above cannot run: the app
pixel only fires on pages Shopify serves, and global mode deactivates
because `/cart.js` doesn't exist on your origin. The order side is
unaffected — checkout still happens on Shopify, and cart attributes still
flow onto the order — so your storefront writes the same attributes the
automatic paths would have written:

1. **Capture on landing.** When a visitor arrives with `karla_*` parameters,
   record them — including the originating-order parameters
   (`karla_order_external_id`, `karla_order_number`, `karla_order_name`)
   that product-promotion links carry — and persist them in your own
   storage, so they survive client-side navigation and are still available
   when a cart is created later. First click wins: don't overwrite values
   you already captured.
2. **Write them to the cart** as soon as one exists, via the Storefront
   API's
   [`cartAttributesUpdate`](https://shopify.dev/docs/api/storefront/latest/mutations/cartAttributesUpdate)
   mutation. Skip the write if the cart already carries `_karla_source`.
   The attributes mirror what global mode writes, prefixed keys and all:

   ```json
   {
     "_karla_source": "trackpages",
     "_karla_campaign": "9f1c6a7e-3b2d-4e8f-9a10-1234567890ab",
     "_karla_medium": "product_promotion",
     "_karla_order_external_id": "8412374597941",
     "_karla_order_number": "1001",
     "_karla_order_name": "#1001",
     "_karla_captured_at": "2026-08-07T10:30:00Z",
     "_karla_landing_url": "https://shop.example.com/products/foo?ref=karla",
     "_karla_landing_path": "/products/foo",
     "_karla_referrer": "https://shop.example.com/pages/tracking"
   }
   ```

   The underscore prefix hides the attributes from the customer in the
   checkout, and each value must stay within Shopify's 255-character
   attribute limit. Strip secrets and PII from `_karla_landing_url` before
   writing it.

3. **Nothing is needed on Karla's side.** Shopify copies the cart
   attributes onto the order as note attributes when the checkout
   completes, and the regular order sync delivers them to Karla as
   `order_analytics`.

:::warning Cart attributes, not line-item properties

Karla reads attribution from order-level note attributes only. Attributes
attached to cart lines (the `attributes` input of `cartLinesAdd` /
`cartLinesUpdate`) end up on the line items, never in the order's note
attributes, and are invisible to Karla. Write them at the cart level with
`cartAttributesUpdate`.

:::

Shopify deletes a cart when its checkout completes, so the next cart starts
without attribution. Re-apply the parameters from your persisted copy when a
new cart appears — how long you keep that copy is your storefront's
attribution window (see below).

### Shopware plugin

The Karla Shopware plugin forwards Shopware's native affiliate tracking:
`affiliateCode` and `campaignCode` on the order are translated into the
canonical `source` and `campaign` fields when the order syncs to Karla. See
the [Shopware guide](/docs/guides/shops/shopware) for plugin setup.

### Direct API

On non-Shopify custom stacks — or whenever your backend is the only place
that knows the attribution — capture the parameters yourself (URL params,
session, server-side) and send them in the `order_analytics` field when you
create or update the order via the
[Orders API](/docs/platform/orders#attribution). On headless Shopify,
prefer [cart attributes](#headless-shopify-cart-attributes); this path is
the fallback when writing the cart isn't an option:

```json
{
  "order_analytics": {
    "source": "trackpages",
    "campaign": "9f1c6a7e-3b2d-4e8f-9a10-1234567890ab",
    "medium": "product_promotion",
    "landing_url": "https://shop.example.com/products/foo?ref=karla",
    "landing_path": "/products/foo",
    "referrer": "https://track.gokarla.io/",
    "captured_at": "2026-08-07T10:30:00Z"
  }
}
```

Use the canonical keys `source` (required for attribution), `campaign`,
`medium`, `landing_url`, `landing_path`, `referrer`, and `captured_at` — they
map 1:1 to what the other capture paths produce. To trace repeat purchases
back to the originating order, also include `order_external_id`,
`order_number`, and `order_name` when the click carried the `karla_order_*`
parameters. The legacy alias keys
`affiliate_code` and `campaign_code` are accepted and rewritten to `source`
and `campaign`. Strip secrets and PII from `landing_url` before sending; the
Browser SDK's sanitizer is a good reference for what to remove.

This path gives you full control (server-side, immune to ad blockers, works
on any platform) at the cost of owning the capture logic yourself.

## Discount code attribution

Attach a discount code to a campaign in the
[portal](https://portal.gokarla.io) and any order using that code is
attributed to the campaign — no parameters involved, so it works on every
platform and survives ad blockers, disabled JavaScript, and cross-device
checkouts.

**Best practices:**

- Keep the code **exclusive to the campaign** — shared or leaked codes
  (coupon sites, social media) inflate attribution.
- Create **unique codes per campaign** and rotate or time-limit them.
- Keep codes simple but distinctive — e.g. `KARLA-WELCOME-DEC` instead of
  `SAVE10`.

**Verification:** filter orders by discount code in your platform (Shopify:
**Orders → Filter by discount code**; WooCommerce: **Orders → Filter by
coupon**).

Discount attribution complements the parameter-based paths: parameters catch
non-discount campaigns, codes catch customers who saw a campaign but
converted later on another device.

## Attribution windows

A captured click doesn't attribute orders forever. Each capture path stores
the click somewhere, and that storage determines the **attribution window**:
which later orders the click can still be matched to. The windows are not
configured on Karla's side — they follow from where each path stores the
click.

| Path                             | Click is stored in                              | Window ends when                                                                                                                                                                                                     |
| -------------------------------- | ----------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Shopify app pixel                | Session storage of the tab                      | A checkout completes (the attribution is sent, then cleared) or the tab's session ends.                                                                                                                              |
| Browser SDK global mode          | The Shopify cart                                | The checkout completes, the cart is emptied, or the cart cookie expires — typically ~14 days after the last cart activity (Shopify doesn't document the exact lifetime).                                             |
| Headless Shopify cart attributes | Your Storefront API cart, plus your own storage | The checkout completes (Shopify deletes the cart) or the cart expires — Shopify expires unused carts within 30 days of creation. Your own persistence decides whether the next cart gets the attribution re-applied. |
| Shopware plugin                  | The Shopware order                              | Not applicable — captured at order time.                                                                                                                                                                             |
| Direct API                       | Your systems                                    | Your choice — you decide how long a click stays attributable.                                                                                                                                                        |
| Discount codes                   | The code itself                                 | Never — the code attributes any order it is applied to, for as long as the code is active.                                                                                                                           |

Two practical consequences:

- **A first click only wins within its window.** Once the window closes
  (order placed, cart gone, session ended), the next qualifying click
  starts a fresh attribution.
- **Changing capture paths changes your numbers.** Migrating your
  storefront — for example from a Shopify theme to a headless setup — swaps
  the attribution window along with the capture mechanism, so
  attributed-order counts can shift even when capture works perfectly.
  Compare like with like before concluding that attribution is broken.

## Verify your setup

Walk the chain once end-to-end:

1. **Click a campaign.** Open your tracking page for a test order, click the
   campaign's call-to-action, and check the address bar: you should see
   `ref=karla`, `karla_source`, `karla_medium`, and `karla_campaign`.
2. **Check the capture.**
   - _Pixel:_ in the storefront tab, open dev tools → **Application →
     Session Storage** and look for the `karla_attribution` key.
   - _Browser SDK global mode:_ add something to the cart, then open
     `/cart.js` in the browser — the `attributes` object should contain
     `_karla_source` and friends.
   - _Headless cart attributes:_ query the cart with your storefront client
     and confirm the cart-level `attributes` contain `_karla_source` and
     friends (not the line-item attributes).
3. **Place a test order** in that same session, then confirm the attribution
   arrived: fetch the order via the API and check `order_analytics`, or open
   the order in the portal.
4. **Check the reporting.** The campaign's numbers appear in
   **Analytics → Purchases** and **Analytics → Campaign Stats** (allow some
   time for processing).

**When something is missing:**

- **No parameters on the click** — the campaign target URL already carried
  its own `karla_*` parameters (yours win), or you tested a link outside a
  campaign.
- **Pixel captured nothing** — the pixel is disabled, the configured consent
  level wasn't granted by your banner, or the link only carried a marker the
  pixel ignores (it records `ref` only for `ref=karla`).
- **SDK captured nothing** — the store isn't a Shopify theme storefront
  (`/cart.js` missing — headless setups need
  [cart attributes](#headless-shopify-cart-attributes)), or the cart
  already carries an earlier `_karla_source` (first click wins).
- **Headless: capture works but orders carry nothing** — the attributes
  were written as line-item properties instead of cart-level attributes, or
  onto a cart that was replaced before checkout.
- **Order has no attribution** — the customer checked out in a different
  browser, device, or session than the click; parameter-based attribution
  can't bridge that gap, discount codes can.
- **Numbers dropped after a storefront migration** — the capture path
  changed, and with it the [attribution window](#attribution-windows);
  verify the new path end-to-end and compare windows before comparing
  counts.
- **Attribution looks too high** — a campaign discount code is circulating
  outside Karla surfaces.

## Where attribution shows up

- **Analytics → Purchases** — attributed orders and revenue per campaign.
- **Analytics → Campaign Stats** — impressions, click-through rate, and
  conversion per campaign; this is also what
  [A/B tests](/docs/guides/portal/campaigns) read.
- **Order data** — `order_analytics` on the order via the
  [Orders API](/docs/platform/orders#attribution).
- **Karla MCP** — the `get_order_campaigns` tool returns the campaigns shown
  for a specific order, so you can query attribution from
  [Claude or any MCP client](/docs/platform/ai/mcp).

:::info Attribution method affects accuracy

Parameter-based attribution undercounts cross-device journeys; discount
codes overcount when codes leak. For A/B tests, prefer parameter or API
attribution, run tests for at least 7 days, and treat ~100+ orders per
variant as the minimum for significance.

:::

## Related

- [Campaigns in the portal](/docs/guides/portal/campaigns) — create and
  manage campaigns.
- [Orders API](/docs/platform/orders#attribution) — full `order_analytics`
  reference.
- [Browser SDK](/docs/platform/browser-sdk) — embed options, including
  global mode.
