Case study
Three months of purchases sent with no identity: retroactive enrichment with a 48-hour retention window
The events had already been sent, and a sent event cannot be edited. We resent them with hashed email and phone without generating a single duplicate, and made the operation repeatable on every future session.
A premium fashion brand on Shopify, high unit price and modest order volumes, freshly migrated from a platform with poor tracking.
The constraint: the events had already been sent. Three months of purchases had reached the Conversions API with correct product data and an empty user-identity section, and a sent event cannot be edited. On top of that the problem was not only in the past: the events upstream of a purchase are anonymous by construction, because the email only appears at checkout.
The result: three months of history resent with hashed email and phone, without generating a single duplicate event, and a service that repeats the same operation automatically on every future session within a 48-hour window.
Below is the mechanism that makes it possible to rewrite an event already received, how we sized the retention window, and what we tell the client before starting.
What this case covers
- how to tell whether a low match quality score depends on how many events you send or on what they contain
- how to resend an already-sent event without it being counted twice
- how to size a data retention window between usefulness and cost
- how to state an undocumented platform behaviour without giving up on using it
Contents
- The client and the starting point
- Recovering the history
- The session enrichment service
- Method note on the numbers
- What didn’t work
- What we would do now
- Conclusion
The client and the starting point
Premium fashion, high unit price, order volumes low compared with a generalist e-commerce store. It is the configuration in which every single conversion weighs more, because there aren’t thousands of events a month absorbing the noise.
The project came out of a migration to Shopify. The new infrastructure was correct: server channel active, browser-to-server deduplication configured, analytics and email marketing platform connected. In the rush to go live, however, the mapping of user data to the Conversions API had been left behind.
The starting position, with the source of each figure:
| Metric | Situation | Source |
|---|---|---|
| Purchase events received | all of them, no errors | Events Manager |
| User-identity section on purchases | empty on all | payload inspection |
| Customer data in the back office | present on every order | Shopify order ledger |
| Duration of the defect | 3 months | comparison of release dates |
The question that opened the engagement came from the client: the Conversions API had been active for three months, every event was arriving, and match quality stayed low.
Recovering the history: resending three months without duplicating
IN SHORT
What we did: rebuilt every purchase from the previous three months out of the order ledger and resent the same event with identity added.
Why: the data existed in the back office and had never reached the advertising platform, so recovery was possible without asking the user for anything.
What we found: native deduplication on event identifier makes it possible to rewrite an event already received.
The number for this section: 3 months of purchases resent, zero duplicate events in Events Manager.
The first check was on quantity, because that is the cheapest one. When a match quality score is low the most common explanation is missing events, and you check it in a few minutes by comparing counts.
The counts matched. Purchases in the back office and purchases received by the platform were aligned. The problem was not quantity.
Every event sent to the Conversions API has two sections. One describes what happened, with value, currency and products. The other describes who did it, with hashed email and phone, name and city. The first was populated correctly, the second was absent.
The technical point that makes recovery possible is that the platform recognises already-received events by their unique identifier. Resending the same event with the same identifier does not create a second one.
The process has four steps. Export the period’s orders with customer data. Hash the personal data to the required standard, with format normalisation applied first. Rebuild the request body with the original identifiers, event names, timestamps and product data, plus the updated identity section. Resend in sequence.
On resend, every original field is preserved, including event timestamp, IP address, user agent and page address. You don’t build a new event that resembles the old one, you resend the old one completed. That is the condition for the platform, having to choose which version to keep in deduplication, to see a coherent body.
What worked
Checking quantity before content. It is the cheapest check and, by ruling out the common explanation, it pointed the rest of the work in the right direction on the first attempt.
The session enrichment service: solving the future too
IN SHORT
What we did: built a service that holds a session’s anonymous events and resends them enriched as soon as an email appears.
Why: you recover history once, but the problem comes back on every future session.
What we found: the browser cookie is a sufficient aggregation key, because it is present on every event.
The number for this section: a retention window of 48 hours, with automatic deletion on expiry.
Recovering the history closed the past. The structural problem stayed open, though, because the events upstream of a purchase arrive before the user has left their email, which only appears at checkout or signup. Every product view and every add-to-cart is born anonymous by construction.
The service receives a parallel copy of every event sent by the server container, stores it indexed by browser cookie, and when a piece of personal data appears within that same session it resends all the preceding events with identity added, using the original identifiers.
Identity accumulates in pieces. Checkout brings the email, a later event may bring the phone, a newsletter signup may bring the name. Every new piece restarts the enrichment of all preceding events with the updated, complete identity.
The retention window is 48 hours. The sizing comes from two opposing constraints. Beyond that point an enriched resend has diminishing returns, because the platform’s useful window narrows. Below it, you lose sessions that come back the next day. Expiry also acts as automatic deletion, so no personal data remains beyond that point and no clean-up procedure is needed.
Consent is handled upstream. If the user has not consented, the tag towards the service does not fire and the service never sees those events. Compliance is guaranteed by the architecture rather than by application logic, which is the configuration with the fewest points of failure.
The service responds immediately confirming receipt and works through a queue, so it adds no latency to the original send.
What worked
Choosing the browser cookie as the aggregation key. It is present on every event, it is the same key the platform uses internally, and it does not require the user to be registered.
Method note on the numbers
Declared source of truth: the Shopify order ledger. The resent events were rebuilt from there, not from the advertising platform’s reports.
| Published number | Source | Window | Model |
|---|---|---|---|
| 3 months of history | Shopify order ledger | from go-live to the intervention | orders with customer data present |
| zero duplicates | Events Manager | after the resend | purchase event count over the period |
| 48 hours of retention | service configuration | per session | automatic expiry on the browser cookie |
The gap between platform data and back-office data on this client was not quantified as a percentage, because the engagement grew out of a problem with event content rather than event counts. The counts were aligned from the start, and that is exactly what steered the analysis.
What we are not able to measure, and with what degree of certainty.
The improvement in match quality after the resend is estimable with low confidence, and we do not publish it. The reason is stated, and it is worth writing out in full, because it concerns the mechanism the whole intervention rests on.
That a resend with the same identifier does not generate duplicates is documented and verified: we checked it on the event count after the operation. That the platform uses the additional data to revisit the match of an event already received is expected behaviour but not stated in the official documentation.
It is an asymmetric-risk choice, and we declare it to the client before starting. If the behaviour is as expected, the historical signal improves. If it isn’t, the resend is discarded in deduplication and produces no effect, positive or negative. With that risk profile the intervention is worth doing, but anyone promising a percentage improvement on this operation is stating something the platform does not publish.
Measuring the effect would take a comparison between two homogeneous populations of events, one enriched and one not, over the same period. That was not set up.
What didn’t work
We released a tracking setup without a check on event content. The user-identity mapping had been left behind in the rush to go live, and the defect got through because the acceptance checks looked at volumes rather than fields. Three months of purchases arrived without identity before anyone noticed. What we did instead: acceptance now includes opening a single event per type and reading it field by field, before looking at any count. A setup that loses events announces itself immediately because the numbers don’t add up; a setup that sends incomplete events never announces itself.
We chose the retention duration without measuring it. The 48 hours were set on an estimate of that store’s typical purchase cycle, not on a real distribution of sessions. It is a reasonable number but an unverified one, and on a business with a longer cycle it would be wrong. What we did instead: the window is configurable rather than hard-coded, so it can be corrected without another release. Measuring the distribution remains on the list.
What we would do now
Measure the real distribution of time between first event and email capture. It is the figure that says whether 48 hours is the right window for this store, or whether 24 would do, or 72 are needed.
Measure the share of sessions actually enriched. The service only works on people who leave an email in the same session. Knowing how big that share is tells you what the intervention is worth, and it is a figure we don’t have today.
Set up the comparison between homogeneous populations. It is the only way to turn the asymmetric risk into a measurement, and it requires holding back a share of unenriched events as a reference for a period.
Extend identity accumulation to returning events. Today aggregation is per session. A user who returns three days later starts from zero, and on a long purchase cycle that is an avoidable loss.
Conclusion
Three months of purchases were resent with the customer’s identity, without generating a single duplicate event, and from then on every session that leaves an email enriches the events that preceded it by itself, within a 48-hour window.
What this engagement leaves behind, picking the four opening points back up.
How to tell whether the problem is quantity or content: you compare counts against the back office first, because it is the cheapest check. If they match, the problem is inside the events.
How to resend an event without duplicating it: you use the same identifier and preserve every original field, adding only the missing part.
How to size a retention window: you weigh the diminishing returns of recovery on one side against the cost of holding the data on the other, and you make the threshold configurable rather than hard-coded.
How to state an undocumented behaviour: you separate what is verified from what is expected, you explain what happens if the expectation doesn’t hold, and you leave the decision with the client with the risk profile in hand.
When this work makes sense, and when it doesn’t
It makes sense if you have modest order volumes and a high unit value, because every conversion matched to a real person weighs more, and if your funnel is short enough for the email to appear in the same session as the events preceding it.
It doesn’t make sense on a high-traffic, low-conversion store, where the overlap between sessions with upstream events and sessions with an email is too small for the recovery to justify the build. And it doesn’t make sense if you expect a guaranteed percentage improvement, because the mechanism it rests on is not stated by the platform.
The first step is to open a purchase event in the platform’s panel and look at whether the user-identity section contains anything. If it is empty, the data is in your back office and it isn’t getting through.