Multi-touch attribution across channels
A converting contact usually crosses several channels before the conversion — an email nurture, an SMS offer, a push reminder. The per-campaign ROAS and revenue attribution view answers “how did this one campaign perform”. The multi-touch attribution endpoints answer the broader question a whole marketing program needs: of this month’s conversions, how is the credit split across the channels — email, SMS, push, RCS — and campaigns that touched the converting contacts? Two endpoints compute this on demand over your CDP event stream; neither needs new storage or instrumentation:POST /cdp/analytics/attribution— allocates each conversion’s credit (1.0 per conversion) across its touchpoints and rolls it up by channel and campaign.POST /cdp/analytics/revenue-attribution— allocates each conversion’s monetary value the same way, then pairs each channel/campaign with the spend recorded on its touch events to report ROAS and ROI.
The dashboard surface
The dashboard surfaces the credit view at Insights → Attribution. Pick a conversion event (for examplepurchased, booked_demo, or subscribed), a lookback window, and an attribution model, then run the report. The pipeline joins every conversion in the (optional) time window to the touchpoints recorded within its lookback, allocates exactly one conversion-credit across those touchpoints under your chosen model, and rolls it up per channel and per campaign. Both endpoints require the contacts:read scope and an owner/admin/developer role, the same gate as the funnel-analytics read surface.
How the join works
For every conversion event inside the window (since … until, defaulting to the trailing 30 days), the endpoint gathers the subject’s touchpoints in the lookback_days (default 30) before the conversion, and the attribution model allocates credit across them. The subject key is COALESCE(contact_id, anonymous_id, user_id) so anonymous pre-purchase touches stitch to the same conversion they led to. Conversions with zero in-window touchpoints still surface — as an unattributed_conversions counter (and, on the revenue endpoint, unattributed_revenue), never silently dropped from the denominator.
The five models, and how to choose
Every model normalises so each conversion contributes exactly 1.0 of credit — the channel and campaign rollups always reconcile to the conversion count, so switching models re-weights the answer, never the sample. The body echoes every figure under the model you asked for, so you can run the same window under two models and compare.
A worked decision flow
Which question are you answering?first_touch. Run this when allocating budget to your introductions:
{ "channel": "email", "credit": 61, "conversions": 61, "credit_share": 0.44 } means email introduced 61 of the converting contacts — 44% of the attributed total — under a wide-window view. Pair it with a long lookback_days so the earliest touch is genuinely the introduction.
Closers — last_touch. The standard “closer” report pairs a short lookback with the latest touch:
sms comes back with a credit_share near 0.6 while email is near 0.1, your closing pressure is on SMS — but remember this model under-credits upper-funnel channels; read it together with first_touch so you do not defund your introductions.
Fairness — linear. The neutral per-touch view, and the default when model is omitted:
time_decay. Rewards recency without discarding the introduction:
t milliseconds before the conversion gets weight 2^(-t / half_life), normalised. At half_life_days: 7, a touch 7 days before the conversion earns half the weight of the moment-of-conversion touch, 14 days before earns a quarter, and so on — so a long window still stacks credit near the close. Shorten half_life_days to concentrate harder on recency; lengthen it to flatten the curve.
Opener + closer — custom (U-shaped). Weight position deliberately:
{ first: 40, middle: 20, last: 40 } reads the same.
Running it over the API
POST /cdp/analytics/attribution returns the credit report. Only owner / admin / developer keys with the contacts:read scope can run it — the report reads the PII-adjacent event stream.
Full request fields:
The response returns
credit_by_channel[] and credit_by_campaign[], each row carrying credit (summed fractional conversions), conversions (distinct), and credit_share of the attributed total — a channel-level view and a campaign-level view come back in one round-trip. total_conversions counts every conversion in the window including the unattributed; attributed_conversions and unattributed_conversions split those with vs without an in-window touchpoint, and total_touchpoints sums the touchpoints the allocation ran over.
Revenue + ROAS report
The revenue endpoint extends the credit body with three optional fields —value_property (JSONB property on the conversion event carrying its monetary value, default "value"), cost_property (JSONB property on touch/send events carrying their spend, default "cost"), and a currency label echoed on the response (no FX; all events in one request must share a currency):
total_revenue, attributed_revenue, unattributed_revenue, total_spend, the overall roas / roi_pct / cost_status, and per-channel + per-campaign ROAS rows. A channel or campaign that spent money but earned no attributed revenue still surfaces (ROAS 0), so wasted spend is visible rather than filtered to winners only. Non-numeric value_property rows are guarded — a single dirty row yields 0 on that conversion instead of failing the whole report.
Guardrails
- Role gate. Both endpoints require an owner, admin, or developer API key with the
contacts:readscope — the report runs over the PII-adjacent event stream, so the gate matches the funnel-analytics read surface. - Lookback is capped.
lookback_daysaccepts 1–365; attribution does not run beyond a one-year window. - Read cap is explicit. Reads are bounded to 50,000 joined touch rows; if the window exceeds that, the response carries
touch_rows_capped: trueand the report is a lower bound — the dashboard renders a banner rather than a silently-rounded figure. Re-run with a tighter window or a narrowertouchpoint_eventswhitelist to raise the ceiling. - The unattributed counter is a signal, not noise. A large
unattributed_conversionsshare usually means conversions are arriving from traffic outside your sending channels — or the lookback window is too tight. - Spend-side of ROAS comes from
cost_property. If your send events don’t stamp a cost, channel ROAS readsno_spendinstead of an infinite ratio.
Choosing a window and model together
A shortlookback_days with last_touch is the standard “closer” report. A long lookback_days with first_touch or linear surfaces awareness. time_decay pairs well with a long lookback because it still rewards the earliest touches while skewing toward the close. If you are comparing channels on a level field, prefer linear; if you are deciding where to add budget, last_touch under-credits upper-funnel channels — read both so you do not defund your introductions.