Email and phone recycling in return fraud
A fraudster who got hit with a chargeback under [email protected] rarely gives up on Gmail. They make [email protected], sign up for a fresh Shopify customer account, and place an order. From the store's perspective, the new customer has zero history. From the fraudster's perspective, nothing has actually changed.
Phone numbers behave similarly. People hoard them. A fraudster might rotate the area code or switch from mobile to landline format, but the underlying digit string survives the customer-account rotation. Same goes for the burner-app phone numbers fraudsters cycle through in larger operations.
This post is about the two cohort signals that exploit the gap. They use hashed identifiers that already exist in RefundSentry's database from the spec 107 PII migration. No new data collection. No new schema. Just two well-placed joins that turn data we were already storing into a fraud signal.
The asymmetry
A clean store database looks like this. CustomerProfile rows are keyed on the Shopify customer GID. Each profile carries a hashed email and phone, plus aggregate counters: return count, chargeback count, last return date, and so on.
When fraud authors create a new Shopify customer account, the CustomerProfile that gets created for them has a fresh GID, a fresh ID, and zero on every aggregate counter. The hashed email and hashed phone, however, are computed from the actual email and phone the fraudster provided, and the actual email and phone do not change between accounts as often as the customer ID does.
Run a SQL query right now: SELECT emailHash, COUNT(DISTINCT shopifyCustomerId) FROM CustomerProfile GROUP BY emailHash HAVING COUNT(*) > 1. On a typical Shopify store with more than a few thousand customers, you will see a non-trivial number of email hashes that map to multiple customer accounts. Most of those will be benign (the same customer signing up twice with a typo, household members sharing an email). Some of those will be fraud authors operating multiple accounts.
The fraud-relevant subset is the one where one of the customer accounts at a shared email hash has a chargebackCount > 0. That is the cohort the new signals target.
What the signals look at
priorChargebackEmail and priorChargebackPhone are mirror signals. They share the same logic, just keyed on different hashes.
When RefundSentry scores a return, the customer-history loader does two extra reads:
- Sum the
chargebackCountacross all CustomerProfile rows in the same shop sharing this profile'semailHash, EXCLUDING the current profile's own count. The exclusion is important: you want the cohort minus self, otherwise the signal would fire on a customer's own legitimate chargeback history. - Same query, keyed on
phoneHashinstead.
The signal then evaluates a graduated tier:
- 0 in the cohort: NOT_TRIGGERED
- 1 in the cohort: TRIGGERED at the 1.0x tier, contributes about 18 base points
- 2 in the cohort: TRIGGERED at the 1.5x tier, about 27 base points
- 3 or more in the cohort: TRIGGERED at the 2.0x tier, about 36 base points
If the current customer has no emailHash (guest checkout, phone-only signup, or a CustomerProfile that was created before the spec 107 hash backfill ran), the signal returns NOT_AVAILABLE rather than NOT_TRIGGERED. Same for phone. NOT_AVAILABLE means "we cannot evaluate this signal on this customer," which is semantically different from "we evaluated it and it did not fire."
The privacy story
The reason this signal can ship at all is that RefundSentry already stores hashed identifiers. The spec 107 migration (May 2026) added emailHash and phoneHash columns to CustomerProfile, computed via SHA-256 over the normalized email and phone. The composite indexes [shopId, emailHash] and [shopId, phoneHash] were added in the same migration, originally to support the cross-shop fraud-ring detection that the team had on the roadmap.
Two design choices in spec 107 are doing the heavy lifting here:
- The hash is deterministic and unsalted. Same email always produces the same hash, across customers and across shops. This is what makes cohort lookups possible. A salted per-row hash would have made cohort detection impossible without storing the salt alongside, which defeats the privacy purpose.
- The hash is non-reversible. SHA-256 is one-way. The hash cannot be turned back into the email or phone, and the index does not need the original. Compliance-wise, the hash is treated like a customer identifier rather than PII, with the same erasure semantics as the rest of the CustomerProfile row.
When a customer asks for erasure, the CustomerProfile row gets deleted. The hash columns cascade away with it. Any cohort signals that referenced the deleted hash will now miss that customer's contribution to the cohort count, which is the correct behavior.
The signal evaluator never embeds the hash itself in the signal-detail payload it returns to the engine. Only the cohort count and a boolean indicating whether the hash was available. A regression test pins this: any future change that accidentally writes the hash into the signal output gets caught.
The shopId-scoping rule
Every cohort lookup is scoped by shopId. The composite index [shopId, emailHash] is the index the query uses, not the cross-shop [emailHash] index. This matters for two reasons:
The first is privacy. A cross-shop join on email hash would leak customer presence information between merchants. Shop A would suddenly know that Shop B has a customer with the same email as one of Shop A's customers. That is a soft form of customer-data sharing that the platform should not enable by default.
The second is signal correctness. A customer who legitimately shops on Shop A and also legitimately shops on Shop B is not, by virtue of that, a fraud risk on Shop A. Treating cross-shop cohorts as fraud signal would systematically over-flag legitimate cross-merchant shoppers. Per-shop scoping keeps the signal a true fraud signal rather than a privacy proxy.
The downside of per-shop scoping is that it does not catch fraudsters who are organized across multiple Shopify stores. A team running fraud at scale across 30 different Shopify merchants will have one chargeback recorded at each store, never two at any single store, and each store's priorChargebackEmail will return NOT_TRIGGERED. That is an acceptable tradeoff for now. A future cross-shop fraud-ring product would address this, but it would need to be opt-in per merchant, with explicit consent, and is out of scope for spec 147.
How this complements the address pivot
The address pivot and the email/phone pivot catch overlapping but distinct fraud subsets.
A fraudster who recycles an address but uses a fresh email and phone every time will be caught by the address-keyed signals and missed by the email/phone signals.
A fraudster who recycles an email or phone but ships to a different address every time (perhaps using package-forwarding services) will be caught by the email/phone signals and missed by the address signals.
A fraudster who recycles all three, address, email, and phone, will trigger all of them, plus the existing customer-history signals if there is enough overlap, plus the post-pass newAccountAtKnownRiskAddress combination signal. The score on that return will saturate the high-risk zone with multiple independent signals contributing, which is exactly the shape we want for high-confidence elevation.
For a deeper look at how these signals interact with the broader detection landscape, see why your Shopify chargeback data is not enough. The fraud-ring perspective is in fraud ring detection beyond the obvious signals.
Edge cases worth knowing about
A few things will surprise merchants who turn this on:
The cohort signal can fire even when the current customer has zero return or chargeback history. That is the whole point. The signal is about the cohort, not the individual. A first-time customer with a clean profile but a shared email hash with two prior chargeback customers will get a 1.5x tier elevation, and that elevation is correct given the data.
The cohort signal will not fire on a guest checkout that has no associated CustomerProfile. Without a profile, there is no emailHash field to look up against. Guest-checkout-specific signals (guestCheckoutReturner) handle this case separately.
The cohort signal counts the prior customers' lifetime chargebackCount, not the time-bounded chargebackCountLast90d. A cohort with old chargebacks still elevates the score. This is by design, fraud authors operate over months or years, and a chargeback from 18 months ago is still a meaningful precursor.
The signal updates instantly when a new chargeback lands at a cohort member. RefundSentry's processChargeback flow updates the CustomerProfile.chargebackCount aggregate in the same transaction as the ChargebackEvent upsert. The next return scored on any cohort member will see the updated count.
Common questions
What happens if my customer's email gets compromised and a fraudster uses it on a new account?
The signal will fire on the new account if the fraudster provides the same email hash. That is a feature: the legitimate customer would not be filing a fraud claim, so the elevated score would not produce a false positive in practice. Merchants who confirm-not-fraud on a return train the engine to weight the signal less heavily for that specific customer cohort.
My customers share emails with their household members. Does this cause false positives?
In practice, no. Household sharing is more common on streaming services than on e-commerce purchase emails. When it does happen, the household members would all need to have separate Shopify customer accounts AND a chargeback on at least one of them. That intersection is small.
Why hash phone instead of storing it in plaintext for matching?
Same reason as email. Plaintext PII in general-purpose tables is forbidden by RefundSentry's data minimization posture. The hash works for matching and supports erasure. There is no signal we can build with plaintext phone that we cannot build with hashed phone, given a deterministic hash function.
Does this signal ever fire on a brand-new merchant who just installed RefundSentry?
Initially, no. The signal needs a non-trivial number of CustomerProfile rows with chargeback events to produce meaningful cohort counts. A brand-new merchant has a few profiles and zero chargebacks, so every cohort lookup returns 0. The signal rate climbs as the merchant accumulates history.
What if a fraudster uses an email alias trick (e.g., Gmail dot tricks, plus-tagging)?
Gmail's + tagging and dot-insertion creates emails that the receiver treats as the same inbox but the email-as-string is different. RefundSentry normalizes the email before hashing, lowercasing and trimming whitespace, but does not currently fold Gmail-specific aliases into a canonical form. A fraud author who knows this trick can defeat the email pivot, though the phone and address pivots remain.
What this means for your store
Two signals, zero new data collection, full privacy compliance. If you have customers in your CustomerProfile table with chargebackCount greater than zero (and you do, every Shopify merchant does), this signal is going to start firing on returns the moment your store's backfill completes.
RefundSentry is free during the private beta. The full pivot signal set ships behind the existing Risk Settings page. You can disable any signal at any time by setting its weight to zero, or scale weights up or down based on what your store sees. See pricing for plan details and the docs for the full signal reference.
The next post in this series, why your Shopify chargeback data is not enough, explains why these cohort signals close a gap that Shopify's native chargeback data structurally cannot.