Attribution is the question of which message gets credit for which outcome, and the answer depends almost entirely on how long your customers take to decide. Most businesses inherit a default window from an analytics tool and never revisit it.
Match the window to the decision
| Business | Decision takes | Sensible window |
|---|---|---|
| Restaurant cancellation fill | Minutes | Same day |
| Appointment confirmation | Minutes to hours | 48 hours |
| Review request | Hours | 72 hours |
| Home services quote | Days to weeks | 30 days |
| Dental unscheduled treatment | Weeks to months | 90 days |
| Real estate enquiry | Months | 180 days, and report it separately |
The long-cycle trap
If your sales cycle is four months and your attribution window is thirty days, messaging will look like it does not work — and you will switch it off shortly before the pipeline it created starts closing. This is the single most common way a working programme gets killed.
First touch, last touch, or both
For a small business the honest answer is usually to record both and report last touch, because last touch is the one you can act on. But keep first touch in the data, because it is what tells you whether messaging is *starting* conversations or merely closing ones that email started.
A programme where messaging is always last touch and never first is doing less than it appears — it is harvesting demand rather than creating it, and that is worth knowing before you scale it.
The practical implementation
- Tag every link with a consistent scheme, so the outcome carries the message identity. The scheme.
- Record the send with its play, contact and timestamp. You cannot attribute what you did not log.
- Record the outcome with its own timestamp, on the same contact.
- Join with an explicit window rather than "most recent", so the window is a decision you can change and re-run.
- Keep an unattributed bucket. Outcomes with no message in the window are real information, not a bug.
-- Last-touch attribution with an explicit window.
with attributed as (
select
o.id as outcome_id,
o.kind as outcome_kind,
m.play,
m.sent_at,
row_number() over (
partition by o.id order by m.sent_at desc
) as recency
from outcomes o
join messages m
on m.contact_id = o.contact_id
and m.sent_at <= o.occurred_at
and m.sent_at > o.occurred_at - interval '30 days' -- the window
)
select play, outcome_kind, count(*) as outcomes
from attributed
where recency = 1
group by play, outcome_kind
order by outcomes desc;Attribution as an explicit, changeable parameter. Re-run it with a different interval and see how sensitive your conclusion is.
Test how much the window is doing
Run the same query at 7, 30 and 90 days. If your conclusion flips between them, your result is a function of the window rather than of the channel, and you need a held-back control group instead of better attribution. How to run that comparison.