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

Send a conversion to the ingest API with orderId + totalAmount at the top level. All UTM params attach automatically.

A conversion is any event that carries orderId and totalAmount as top-level fields on the ingest API request. Everything else is optional — but the richer the payload, the deeper the reports.

!
Conversions cannot be sent with the browser helper Sumidata.push('event', ['purchase', {}]). The SDK keeps only _category / _feature / _surface at the top level and moves every other property into payload — so orderId and totalAmount would land in payload and never register as a conversion. Send the event to POST /sdk/ingest with the fields at the top level, from your backend or a direct browser fetch.
conversion.ts
await fetch('https://api.sumidata.io/sdk/ingest', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    projectId: 'proj_…',
    deviceId: sumidataDeviceId,    // SDK device id (or zero-UUID)
    sessionId: sumidataSessionId,  // optional — links UTM + experiments
    externalUserId: user.id,      // same id you pass to identify()
    source: 'realtime',
    events: [{
      name: 'purchase',
      orderId: 'ord_01HW…',
      totalAmount: 79.99,  // net, after discount
      currency: 'USD',
      productId: 'plan_pro_monthly',
      quantity: 1,
      isPrimarySale: true,
      _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 — send one event per line item in the same events array, all with the same orderId and a distinct productId:

cart-checkout.ts
events: cart.items.map(item => ({
  name: '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 is skipped — the duplicate never reaches storage, so there is no double-counting if the thank-you page reloads, and it is reported in the skipped array of the response. 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 and still stores the rest.

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 · pdp · checkout · … (98 total — see the Product events guide)
_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 do not fail the request — the offending event is skipped and returned in the skipped array of the response, while every valid event in the same batch still stores. Keep your taxonomy consistent — the first event you send with a given name sets the metadata that powers future funnel queries.

07Where to send conversions

Conversions always go through the ingest API. What changes is where you call it from.

Every conversion is an /sdk/ingest call with the fields at the top level — the browser push('event', …) helper cannot carry them (section 02). Call the ingest API from your backend when the source of truth lives on your server (webhooks, subscriptions, renewals, invoicing). Call it with a direct browser fetch when the money moves in the user's tab and you want to fire from the thank-you page — read the SDK's deviceId and sessionId first so attribution links.

ScenarioCall ingest from
Synchronous checkout, user sees thank-you pageBrowser fetch → ingest
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
Conversions inherit UTM attribution either way — pass the user's sessionId (or stored UTMs) when you send the event. From the browser, grab it via window.Sumidata.getSessionId() and localStorage.getItem('sumidata_device_id'). See Linking browser session to server.