Sumidatasumidata.io
Sign in
Docs/Guides/Conversions & attribution
Guides · Conversions

Conversions & attribution

Track revenue with full UTM attribution, multi-touch journeys, and automatic deduplication. No extra configuration — UTM params from the landing URL flow through to every event fired in the session.

01UTM capture

UTM parameters on the landing URL are captured once per session.

The SDK reads the standard five UTM parameters from the initial landing URL and attaches them to every subsequent event in the session:

  • utm_source — where the click came from (google, newsletter, partner-x)
  • utm_medium — channel class (cpc, email, social)
  • utm_campaign — campaign identifier (spring-sale-2026)
  • utm_term — keyword or audience slice
  • utm_content — creative / ad variant
i
UTM params persist for the full session. If the user navigates to other pages without UTM in the URL, events still carry the original attribution.

02Tracking a conversion

Fire a conversion event when revenue happens. All UTM params attach automatically.

A conversion is any event with an orderId and totalAmount. Everything else is optional — but the richer the payload, the deeper the reports.

checkout.js
Sumidata.push('event', ['purchase', {
  // --- required
  orderId: 'ord_01HW…',
  totalAmount: 79.99,  // net, after discount
  currency: 'USD',

  // --- product detail
  productId: 'plan_pro_monthly',
  productName: 'Pro (monthly)',
  productCategory: 'subscription',
  quantity: 1,
  unitPrice: 79.99,

  // --- coupons & discounts
  couponCode: 'LAUNCH20',
  discountAmount: 20.00,

  // --- lifecycle
  isPrimarySale: true,  // user's first-ever purchase

  // --- AAARRR tagging (optional, powers funnels)
  _category: 'revenue',
  _feature: 'checkout',
  _surface: 'app'
}])
i
Send totalAmount as the net after discount, and put the discount amount in discountAmount. Sumidata reports show gross / discount / net separately when both are present.

Multi-line-item orders — fire one event per line item, all with the same orderId:

cart-checkout.js
for (const item of cart.items) {
  Sumidata.push('event', ['purchase', {
    orderId: order.id,                      // shared across items
    productId: item.productId,               // unique per item — dedup key is (orderId, productId)
    quantity: item.quantity,
    unitPrice: item.unitPrice,
    totalAmount: item.lineTotal,
    currency: 'USD'
  }])
}

03Multi-touch attribution

For accounts with multiple sessions before they convert, Sumidata attributes across the journey.

When a user is identified (via identify()), all prior anonymous sessions for that device are stitched. The conversion event gets the full journey attached:

  • first_touch — UTM of the first session where we saw this user
  • last_touch — UTM of the session the conversion fired in
  • touches — array of all sessions in between, ordered

04Deduplication

A conversion fired twice (e.g., the thank-you page loads on refresh) counts once.

The dedup key is (orderId, productId). Firing purchase twice with the same pair returns 400 and never reaches storage — no double-counting if the thank-you page reloads. For multi-line carts this means each line item is deduped independently, so a retry of a partially-sent cart drops only the already-ingested items.

i
If productId is omitted, the dedup key is (orderId, '') — which still works for single-product orders. Always send productId when you have one, so multi-item orders don't collide.

05A/B experiments

Bind A/B variants to the session, then every conversion joins the correct group automatically.

Register the variants the user is seeing — once per session, as soon as your experiment framework hands you the assignments. All conversions fired afterwards (browser or server) are attributable to those variants for lift analysis. For the full guide — stickiness rules, GrowthBook / Statsig / LaunchDarkly / Optimizely recipes, query patterns, limitations — see Experiments.

experiments.js
// Called once per session, after your A/B framework resolves assignments
Sumidata.push('identifyExperiment', [[
  { id: 'pricing-v3', variant: 'control' },
  { id: 'onboarding-flow', variant: 'short' },
  { id: 'hero-copy', variant: 'v2' },
]])

Sticky assignments

First assignment per (session, experimentId) wins. Calling identifyExperiment again with a different variant for the same experiment is a no-op — the original variant stays. This protects your analysis from flicker-reassignment bugs.

What you getExperiment outputs

  • Revenue and conversion-rate splits by variant, per experiment.
  • Funnel drop-off by variant — which step hurts each group.
  • Session replays filterable by variant — watch 5 "treatment" sessions vs 5 "control" side-by-side.
i
Experiments are bound to the browser sessionId. Server-side conversions that link back to the same session (via sessionId in the ingest payload) inherit the variants automatically. See Linking browser session to server.

06Event metadata (AAARRR)

Tag events with AAARRR stage, feature, and surface so funnel reports stay coherent.

Three optional properties turn raw events into analyzable funnels. Set them once per event and Sumidata auto-maintains a dictionary for query-time joins.

FieldAllowed values
_categoryawareness · acquisition · activation · revenue · retention · referral
_surfacemarketing_site · app · mobile_app · docs
_featurefree-form, [a-z0-9_]+
events.js
// A signup — acquisition stage, on the marketing site, auth feature
Sumidata.push('event', ['signup', {
  _category: 'acquisition',
  _feature: 'auth',
  _surface: 'marketing_site'
}])

// A purchase — revenue stage, checkout feature, in the app
Sumidata.push('event', ['purchase', {
  orderId: order.id, totalAmount: order.total, currency: 'USD',
  _category: 'revenue', _feature: 'checkout', _surface: 'app'
}])
i
Invalid values are rejected at ingest with a 400. Keep your taxonomy consistent — the first event you send with a given name sets the metadata that powers future funnel queries.

07Browser vs server conversions

The browser SDK covers most checkouts. Server-side ingest covers the rest — and they compose.

Fire from the browser when the money moves in the user's tab (hosted checkout, Stripe Elements, a client-only flow). Fire from your backend when the source of truth lives on your server — webhooks, subscriptions, renewals, invoicing, anything asynchronous to the browser session.

ScenarioWhere to fire
Synchronous checkout, user sees thank-you pageBrowser SDK
Stripe / PayPal / provider webhookServer-side ingest
Subscription renewal, dunning recoveryServer-side ingest
Wire transfer / invoice paid out-of-bandServer-side ingest
Offline / in-person sale entered by staffServer-side ingest
Replaying historical orders (first integration)Backfill
i
Server-side conversions still inherit UTM attribution — pass the user's original sessionId (or stored UTMs) when you send the event. See Linking browser session to server.