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 sliceutm_content— creative / ad variant
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.
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'
}])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:
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 userlast_touch— UTM of the session the conversion fired intouches— 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.
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.
// 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 get
- 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.
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.
| Field | Allowed values |
|---|---|
_category | awareness · acquisition · activation · revenue · retention · referral |
_surface | marketing_site · app · mobile_app · docs |
_feature | free-form, [a-z0-9_]+ |
// 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'
}])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.
| Scenario | Where to fire |
|---|---|
| Synchronous checkout, user sees thank-you page | Browser SDK |
| Stripe / PayPal / provider webhook | Server-side ingest |
| Subscription renewal, dunning recovery | Server-side ingest |
| Wire transfer / invoice paid out-of-band | Server-side ingest |
| Offline / in-person sale entered by staff | Server-side ingest |
| Replaying historical orders (first integration) | Backfill |
sessionId (or stored UTMs) when you send the event. See Linking browser session to server.- Server-side ingest → full API reference, attribution patterns, experiments, errors, config.
- Historical backfill → replay past orders with correct first-time-buyer flags.
