Littledata MonitorAI guide

Klaviyo flows — what the audits mean

These checks review live flows, triggers, exclusions, and revenue attribution so Littledata server-side events work cleanly next to native Klaviyo tracking.

How to read results

Outcome Meaning
Error Likely hurts revenue capture, doubles messaging, or hides a competitor issue.
Warning Worth fixing.
Info FYI.
Success Rule passed.
Unknown Missing data, no flows, or nothing to compare.

Littledata abandonment flows

What we look for: Live flows whose triggers use exactly these three Littledata metric names:

Rules:

Outcome Rule
Success All three have at least one live flow.
Warning One or two covered.
Error None covered.
Unknown No flows visible in the account.

Caveats: Renamed triggers are not detected. Email/SMS content is not reviewed.

Performance vs peer benchmark (enrichment): at value-compute time (customer and prospect paths), each trigger is also benchmarked against the size-aware peer expectation — the store’s own deepest live Littledata flow anchors the scale, multiplied by the cohort-median uplift ratio between stages. A live flow earning under 50% of its peer-expected uplift is flagged as underperforming with the dollar gap; a missing Littledata flow gets an “activate to add ~$X/mo” estimate. The result is appended to the check’s message/furtherInfo and stored in structured details (flowPerformanceVsBenchmark, potentialMonthlyUplift). The red/amber/green status is unchanged — it still reflects on/off coverage only. Requires the measured-uplift predicted tier; peer-modeled tiers are never used to judge per-flow performance.


Native abandonment flows (coverage by stage)

What we look for: For each funnel stage—browse, cart, checkout—whether there is either a live native Klaviyo flow or a live Littledata flow (same three pairs as above). Littledata-only setups should not be punished for “missing native” if the Littledata stage is live.

Rules:

Outcome Rule
Success Every stage has native or Littledata live.
Warning At least one stage has neither.

Caveats: Only Warning when a stage is empty (no Error here). Does not check send timing or creative.


Active SMS Flows

What we look for: Whether any flow actually sent on the SMS channel in the last 30 days. Channel activity comes from Klaviyo’s flow values report grouped by send_channel — the same single call that syncs flow revenue, so this costs no extra API requests. Klaviyo labels channels email, sms, whatsapp and push-notification.

Rules:

Outcome Rule
Success At least one flow sent SMS (recipients > 0).
Info Flows are sending, but none on SMS — an email-only store. The message frames SMS as an opportunity: around 1 in 3 Klaviyo accounts run SMS flows.
Unknown The report was unavailable, or no flow sent on any channel.

Caveats: This is sending, not configured — an SMS flow that is drafted, paused, or has an empty audience shows up as Info. Volume is not judged: a flow reaching 5 recipients still counts as Success. WhatsApp and push activity are recorded in the check details (channels) but do not change the status.


Active competitor flows

What we look for: Flows triggered by metrics that reference rival tracking tools. Detected names include (examples): Elevar, Blotout, Wunderkind, Black Crow, Addingwell, Wetracked, Aimerce, Fueled, Upstack, Stape, Triple Whale, Polar, ThoughtMetric, TrackBee, Tracklution, Venon, plus Retention.com’s Reclaim by product name (matched case-insensitively in the metric name or the metric’s integration label when the title alone is generic — see Detection below).

The bar is “would Littledata replace it as the trigger source?” — a detected tool becomes an “upgrade from {competitor}” pitch and anchors a predicted-revenue tier on its own flow revenue. Tools that fire the same abandonment metrics but run alongside Littledata are deliberately not detected: Customers.ai (builds purchase-intent audiences), Opensend and usemate.ai (identity resolution that surfaces shoppers in addition to the ones we track; usemate.ai tags its metrics [Checkmate]) and Digioh (popups).

What we compare: For live competitor flows we can map to the same funnel stage as Littledata, we compare Klaviyo-attributed Placed Order revenue in the same rolling window Littledata uses for its own flow revenue—then we factor in whether the flows exclude each other’s audiences so double counting is less likely.

Comparable stages: A row counts toward the aggregate only when the shop has a paired Littledata flow and the competitor’s revenue in that window is not below our low-volume floor (so noise does not dominate).

Rules:

Outcome Rule
Success No competitor flows found or (live + comparable stages exist and Littledata’s total attributed flow revenue is ≥ competitors’ total across those stages).
Info Competitor flows exist but are not live (draft/manual).
Warning Live competitor flows, but we have no comparable stages to score (for example, every pair is unmapped, missing a Littledata flow, or the competitor is low-volume).
Error Live + at least one comparable stage, and Littledata’s total is less than competitors’ total across those stages.

Per-row detail: The audit’s furtherInfo still shows one line per funnel stage (red/green bullets) so a single losing stage stays visible. Headline status is not “any one losing pair = Error”; it is the aggregate comparison above.

Waterfall (multi-flow) setups: If the shop uses a cascade of triggers (for example, native “Added to Cart” → a competitor’s ATC → Added to Cart – Littledata), we still infer Littledata’s position from exclusion metadata (trigger filters, profile / metric conditions, conditional splits). That inference shapes per-row bullet tone in furtherInfo (for example, when Littledata is downstream, higher revenue on a competitor line may be expected). It no longer overrides the Error vs Success headline by itself; the headline follows aggregate revenue on comparable stages.

Head-to-head benchmark (background): When a shop runs both Littledata and a competitor flow at the same stage (dual-active), we also write a dated head-to-head snapshot (LD vs competitor revenue, keyed by waterfall position) and, when a previously-dual shop later drops to one source, a consolidation outcome (which source it kept). These accumulate over a rolling 12 months and feed the prospect-facing “upgrade from {competitor}” value estimate — see Value metrics. They don’t affect this check’s status.

Detection: We match the competitor’s brand anywhere in the trigger metric’s title or its Klaviyo integration name — plus, for vendors where the brand alone doesn’t settle it, the individual product. Retention.com is the worked example: it sells identity resolution (additive, like Opensend) and Reclaim, which supplies the abandonment triggers a store’s flows fire on — Littledata’s own job. So we match “Reclaim” and deliberately not the bare brand, which wouldn’t say which product a metric came from. A product match is still reported as the company, because that is what customer-facing copy names.

Caveats: Name-based detection misses white-label tools. Renamed triggers may disappear from detection. The team may get a Slack heads-up when competitors are first seen.

Competitor audience uplift (separate check)

What it is: A follow-on row that compares unique profile reach (audience) at each funnel stage when competitor flows or competitor metrics exist, using the same comparison window as revenue.

Stages compared:

  1. Live-flow stages — every funnel stage where a live competitor flow already drives the revenue comparison.
  2. Metric-only stages — funnel stages where the competitor metric is firing (e.g. via the competitor’s tracking integration) but no live competitor flow exists. These rows are tagged “(no live competitor flow)” in furtherInfo so they’re visually distinct, and they roll into the audience aggregate. They do not affect the revenue check.

Rules: We compare total Littledata vs total competitor unique profiles (same comparison window) across both row types—not “fail if any single stage loses”. Success when Littledata’s aggregate reach is ≥ the competitors’ aggregate; Info when the aggregate is behind or profile snapshots are not yet available. Per-row lines in furtherInfo still show each stage so individual gaps stay visible.


Flow exclusion rules (Littledata vs native)

Why it matters: If both Littledata and native Klaviyo triggers are live for the same action, both flows can fire unless one flow excludes people who already entered the other.

What we check: For each parallel pair we expect, one side should reference the other’s metric in a trigger filter or profile / metric style condition so the same shopper is not double enrolled. The exclusion is bidirectional—it counts whether the Littledata flow excludes the native metric or the native flow excludes the Littledata metric (we look for the metric name or ID Klaviyo exposes—not whether the English sentence is perfect). A pairing is only reported when neither direction fences it off.

Rules:

Outcome Rule
Success Every trigger with a live Littledata flow is safe — either fenced off on at least one side, or one side is paused, drafted, or no longer sending (so there is nothing to collide with).
Error At least one sending Littledata flow paired with a sending native flow where neither direction excludes the other.
Unknown No live Littledata flow sits on a trigger we can pair.

Every sending flow on each side counts, not just the first: an account can run several live flows on one trigger — a rebuilt flow next to the shell it replaced, or per-market variants. We evaluate all sending flows on both sides and report only the pairing that is genuinely unguarded. Judging the trigger on whichever flow happened to come back from Mongo first produced false Errors: UK Flooring Direct rebuilt its Littledata flows and left the originals at status: live with 0 recipients and no exclusion metadata, so the check read a flow that sends nothing while the flow actually sending (972 and 244 recipients) did exclude the native metric.

Flow-level exclusions that name the flow count. A filter such as “Received Email where $flow is zero times” fences the pair off just as well as a trigger-metric exclusion, and we read it from `gating.excludedFlowIds` (bare id, or `Name (ID)` as some definitions carry it). Before this, half the pairs the check reported (17 of 34 in Sep 2026, every pair on 12 of 23 stores) were guarded exactly this way.

Market splits are a caveat, not a pass. When both flows filter on a country / locale property, the furtherInfo line says the pair is fine if they target different countries. We store which property a flow filters on, not its values, so we cannot tell a UK/US split from two flows that both reach the UK.

Caveats: We cannot read every creative nuance of filters; we flag missing references, not “wrong logic.” Klaviyo’s bare profile-not-in-flow condition (“has not been in flow X in the last N days”) is invisible to us: it names no flow in the definition API — all 1,479 occurrences across a 20,000-flow scan carry {type, timeframe_filter} and nothing else. That is why the check’s message asks for a filter we can see.

Why pausing counts as Success: merchants resolve an Error here either by adding an exclusion filter or by pausing a flow. Both remove the double-trigger risk, so both must land in Success — a status of Unknown skips the resolvedAt stamp that the admin UI’s status badge uses to show “Resolved …”, which makes a real fix look like a check that stopped working. This applies to either side going quiet: pairsWithExclusion[].reason records which (exclusionRule, noActiveNativeFlow, noActiveLittledataFlow).

We recursively walk condition_groups in trigger filters, the flow-level profile_filter, and conditional split profile filters, and we collect metric IDs and string values (e.g. on event_name conditions) so exclusions are detected even when Klaviyo nests groups. The union of triggerMetricId values for a trigger (both the native and the Littledata side) also includes non-live flows (same metric name) so a parallel draft locale does not hide a metric id the opposing flow already excludes. Re-sync flows (Klaviyo audit) is required to refresh profileFilterMetricIds / filterTriggerValues in Mongo after logic changes or flow edits.


Littledata flow deliverability (bounce rate)

Why it matters: A flow that emails addresses which do not exist bounces, and mailbox providers read a high bounce rate as a sign the sender is not looking after their list. The cost is not limited to the flow: once a provider like Microsoft or Google starts distrusting a sending domain, every email from it — campaigns included — is more likely to be blocked or filtered to spam. So a single badly-behaved flow can quietly degrade an entire Klaviyo account.

Littledata’s abandonment triggers fire server-side, from the store, which is the reach we sell: they reach shoppers who abandoned without the storefront ever capturing them. That same breadth means they can also reach an address that was typed at checkout and never verified — including addresses entered by bots. Klaviyo’s own triggers fire from the storefront, so they only ever reach addresses the site captured. This check exists because the difference between those two audiences is ours to answer for.

What we check: For each live Littledata-triggered flow, the share of its emails that bounced over the last 30 days, compared against the merchant’s own flows over the same period — first the native Klaviyo flow on the same trigger, failing that the rest of the account. The comparison tells a Littledata problem apart from an account-wide one. Then, for any flow over the warning line, we read the bounced addresses themselves off Klaviyo’s Bounced Email events (newest 2,000, this flow only) to say why it is bouncing — see Causes below.

Rules:

Outcome Rule
Success Every Littledata flow with enough sends to measure bounced under 10%. The message names Klaviyo’s own 2% guideline and says which side of it the flows are on, so a store sitting at 7% is not told it is “comfortably inside” anything.
Warning A Littledata flow bounced 10% or more. Three variants, recorded as details.verdict — they change neither the copy nor the severity: at least three times the merchant’s own flows (specificToLittledata — the trigger’s audience); the rest of the account bounced just as hard (listWide — the addresses reaching every flow); or nothing else in the account sent enough to compare against (aboveTolerance).
Unknown No Littledata flow sent at least 250 emails in the window, so no rate can be measured.

There is no Error. ERROR is the status that reads as an incident and opens a ticket, and this check has no half that is ours: every lever it names — Bot Protection, a domain filter on the flow, verifying new profiles, a send delay — lives in the merchant’s Klaviyo account. So the worst status is Warning and the check is in linearSuppressedChecks. It stays fully visible to customers, prospects, Slack and marketing automations, with the help article beside it; it just never queues work we cannot do.

Why 10%, and not Klaviyo’s 2%: 2% is Klaviyo’s published guideline, and we cite it — but it is the wrong line to alert on here, because a Littledata flow is where the account first emails an address typed at checkout, and a first send to an unverified address bounces about 3× more often than a send to an established profile (see the benchmark below). Grading at 2% would fire on the reach we sell, at stores with nothing wrong: of 31 flows observed starting above 2%, the median fell 1.88 points in five weeks with nobody touching them. 10% is more than an order of magnitude above the fleet median (0.29% on Littledata flows, 0.41% on the merchant’s own), so a flow that clears it is a flood rather than the ordinary cost of mistyped checkout addresses. Past the line the comparison decides whose problem it is; the addresses decide the advice.

Causes, and what the merchant is asked to do. A bounce rate is a lagging count of a consequence — it says the sends went badly, not why. Measured across the customer base neither Bot Protection (on: 0.27%, off: 0.25%) nor the flow’s re-send frequency predicts a high rate, and 2 of the fleet’s 3 worst-bouncing flows already had Bot Protection on, so a check that only asked for Bot Protection would have asked those merchants to do what they had already done. Reading the bounced addresses gives one of four causes, each with an action a merchant can actually take:

Cause How we recognise it The ask
Bot checkouts ≥40% of bounced addresses follow the machine-generated first.last####@ shape, or the provider mix is spread too evenly over a handful of free providers to be organic (the same tests as the Spam profiles check). Corroborated by the Spam profiles created and Bot checkouts checks when the addresses could not be read. Bot Protection off: turn it on for the Klaviyo connection, and remove the fake profiles already in the account. Bot Protection on: the bots are passing as real browsers, so the only remaining lever is a filter on the flow (e.g. requiring email consent) — and we say it costs reach.
Marketplace relay addresses ≥25% on relay or placeholder domains (marketplace.amazon.*, members.ebay.*, example.com, noemail…), which never accept email. Add a profile filter to the flow excluding those domains.
Soft bounces Of the bounces Klaviyo labelled, a majority are soft (full or temporarily unavailable mailboxes). Nothing: Klaviyo retries and suppresses repeat offenders.
Ordinary addresses None of the above: ordinary-looking hard bounces, i.e. addresses mistyped at checkout. Two things make that measurable: domains one letter off a major provider (gmail.co, hotmal.com), and the bounce being the first email ever sent to a profile created in the previous two days. On the account this was built from, 51 of 70 bounces on the Littledata flow were first sends and gmail.co was its second most common domain. Verify new profiles before their first email (mailfloss’s Instafloss suppresses the ones that fail) and keep a delay of at least an hour on the flow. The typo domains and first-send counts stay in details.bounceComposition. (This used to say “nothing; Klaviyo suppresses each address once it bounces”, which is true and is not an action: it does nothing about the next batch.)

When the addresses could not be read (no client, no Bounced Email metric, fetch failed) the check falls back to the bot checks; with nothing from them either, it asks for Bot Protection where it is off and says it could not read the addresses where it is on.

The trend. Each flow’s bounce figures are kept as a short weekly series (about three months), so the check compares today’s rate against the same flow roughly a month ago: falling (the source has stopped and the bad sends are ageing out of the 30-day window — confirm it keeps falling), rising (new bad addresses are still arriving), or holding. The comparison reading must be 28–70 days old and over the same 250-email floor. A falling rate earns a clause in the message (“…but the rate is already falling”), because the merchant’s job is then to watch rather than act; a rising or holding rate adds nothing, since the action is the same. The direction no longer moves the status, since there is no Error to come down from. A flood clears because its source stops, not because Klaviyo suppresses addresses — suppression only prevents a repeat send to the same address, and a checkout flow mails mostly new ones.

Message and working. The message is two sentences — what is bouncing, then the one action: "<flow>" bounced X% of its emails, mostly to bot addresses. Turn on Bot Protection for your Klaviyo connection and remove the fake profiles already in your account. A clean row is one sentence saying which side of Klaviyo’s 2% guideline the flows are on. furtherInfo carries the evidence behind that claim, so the merchant can check it before acting: each Littledata flow’s own bounce rate and counts (worst first, at most five, then “+N more”; a flow under 250 sends is marked too few to judge), the baseline it was compared against, the earlier reading when the message says the rate is falling, the bounced-address breakdown (hard/soft split from Klaviyo’s Bounce Type, machine-generated, relay and placeholder, first-send and one-letter-off-domain counts, sender-reputation rejections), the Spam profiles created and Bot checkouts results, the Bot Protection state and spam complaints. Method stays out of it and in details for admins: the first-send benchmark, the most common domains, Klaviyo’s raw bounce categories, the comparison multiple and where the 10% line comes from.

Why a Littledata flow bounces more than the native twin, when the addresses are ordinary. The Littledata flow is where the account first emails an address nobody has verified: the trigger fires server-side for every checkout, so a brand-new profile gets its first ever email from this flow. The native twin’s audience is profiles Klaviyo already knew — identified onsite, so previously mailed — and any address that bounced before is already suppressed and never enters it. The native flow inherits a population that has survived a first send; ours is the first send. That is the price of the reach, not a defect. The check measures it per flow (first-send share of the bounces) and, when the flow’s bounces are mostly first sends to ordinary addresses, stores the fleet benchmark below in details.firstSendBenchmark.

The first-send benchmark refreshes itself. Every nightly Klaviyo audit contributes one per-account sample at most once a week (klaviyoFirstSendSamples: the newest 100 profiles created 3–33 days earlier with their suppressions, and the newest 200 Bounced Email events with profile creation dates — two Klaviyo calls). The figure quoted is the median across accounts’ samples from the last 45 days, because a handful of accounts under a bot flood run 50–80% new-address bounce and would swamp a pooled figure. Until at least 10 accounts have contributed it falls back to the seed measured on 16 Sep 2026 with scripts/benchmark-first-send-bounce.ts (40 accounts, 18,656 new profiles); the stored details say which (firstSendBenchmark.source).

Measure (seed, 16 Sep 2026) Figure
Addresses created 3–33 days earlier that bounced on their first email 1.8% (a floor — never-mailed profiles dilute it)
Bounced ÷ recipients over 30 days across each account’s flows 0.46%
Ratio, per account 3.1×
Share of a Littledata flow’s bounces that were first-ever sends 86% (19 accounts with ≥10)
Same share for other flows and campaigns 3% (39 accounts)

What we learned from the whole customer base (10,330 deliverability snapshots, 374 Klaviyo accounts, March–April 2026): Littledata flows bounce less than the merchant’s own (median 0.29% vs 0.41%); only 8 Littledata flows in 372 sat at 5%+ with 10+ bounces, on 5 accounts, and 6 of the 8 had no native twin to compare against; checkout flows bounce ~2× cart and browse on both sides because checkout is where an address is typed by hand; of 31 flows starting above 2%, the median fell 1.88 points in five weeks untouched.

Guards against false alarms: A flow is only graded once it has sent 250 emails — at a 10% line that is also the count floor, since 250 sends cannot reach 10% on fewer than 25 bounces, so the separate 10-bounce floor the 2% line needed is gone; the comparison flows must clear the same 250-email floor or the rate is reported without attribution; a cause is only named from 10 or more bounced addresses; paused and draft flows are ignored on both sides; a flow whose bounce figures have not yet synced is left out rather than counted as having bounced nothing.

Cost: DB-only for the rate. Reading the addresses costs one metrics call and up to ten pages of Bounced Email events, and only for a flow already over the warning line — single digits fleet-wide.


Littledata flow gating parity

Why it matters: Merchants build their Littledata flow by duplicating the flow they already run. When the copy ends up gated differently from the original, it emails people their own flow would have held back, emails the same person more often — or, in the other direction, reaches fewer shoppers than their own settings would. Reporting the difference against their own flow is the one comparison that carries no opinion about how a store should gate its email.

What we check: For each live Littledata-triggered flow, we compare its send settings against the merchant’s own live flow on the same abandonment stage — the busiest one, if they run several. Five things are compared: the consent conditions on each channel; messages marked transactional (which bypass marketing consent); the conditions on past events in the flow’s profile filter (Bounced Email, Placed Order, and so on — the case this check was built from had a native flow that excluded anyone who had bounced in 180 days and a Littledata copy that did not, which is exactly the filter that decides whether a bad address gets mailed); how soon the flow will re-enrol the same shopper; and which other flows it excludes recent members of.

We compare both ways. Our flow being looser is the headline finding. Our flow being tighter is reported too, as information: reach is what Littledata sells, so a flow of ours that waits four times as long to re-enrol a shopper, or requires a past-event condition theirs does not, is quietly delivering less than they pay for.

And against two standards. When the merchant has no flow of their own on the stage, we fall back to Littledata’s own template, which holds an opinion on one setting only: an abandonment email is marketing, so a message marked transactional is wrong on it under any list policy. Consent, past-event filters and re-enrolment windows are deliberately left out, because there is no universally right value for any of them.

Rules:

Outcome Rule
Success Every Littledata flow is gated the same way as the merchant’s own flow on the same stage.
Error The Littledata flow sends transactional messages where theirs sends none (or where our template says none).
Warning Theirs requires consent on a channel and ours does not; or theirs has a condition on past events ours lacks; or ours is missing an exclusion their flow has, so a shopper can receive both flows’ emails for one abandonment.
Info The Littledata flow re-enrols a shopper sooner than theirs (at least twice as soon, and at least 3 days) — or is tighter in any way, reaching fewer shoppers than their own settings would.
Unknown The account runs no Littledata-triggered flow at all.

Why consent is a Warning and not an Error: dropping the consent gate from the Littledata copy changes who gets mailed, but reaching shoppers the native trigger never identified is why merchants install Littledata, so the omission is plausibly deliberate. In the 12 accounts with gating stored so far, no abandonment flow on either side gates email on consent, so the finding will be rare; when it fires, the message says “Add one unless that was deliberate”. The transactional flag is different — it bypasses consent Klaviyo would otherwise enforce, and no list policy makes that right on a marketing email.

Why frequency is only information: measured across 372 graded Littledata flows, those re-enrolling at least twice as soon as the merchant’s own bounced a median 0.34% against 0.26% for the rest, with none above 2% — Klaviyo suppresses an address after its first bounce, so re-sending to the same shopper cannot bounce them twice. Frequency drift is still drift from what the merchant configured, so it is worth saying; it is not a deliverability problem.

Message and working. The message is at most two sentences: the worst flow, what differs from their own flow, and the change to make — e.g. "<flow>" has no email consent check, unlike your own "<flow>". Add one unless that was deliberate; 9 other Littledata flows differ too. furtherInfo lists, per Littledata flow, only the changes to make (🟠), capped at five flows then “+N more”; the 🔵 notes (re-enrols sooner, tighter than theirs) appear only when nothing needs changing. Matching flows and flows not yet re-synced are left out, and furtherInfo is omitted when it would only restate the message (one flow, one change). A clean row is one sentence; where the only basis was the template, it claims only that no transactional messages are sent.

Guards against false alarms: The comparison flow must have actually sent in the last 30 days. Exclusion windows are only reported when the difference is both proportionally large and at least 3 days. Excluded flows and past-event conditions are compared by identity, not by count. Past-event conditions are compared by metric name once both flow docs carry names (metadata v5) and by id until then, so a half-migrated account never reports a gap between a metric and its own id. Paused and draft flows are ignored on both sides.

Caveats: This reads the flow’s configuration, not its sends, so it reports drift the moment it appears — before any bounce shows up, and whether or not one ever does. It reports the metric a condition is on, not its operator or window: “excludes anyone who bounced in 180 days” and “excludes anyone who bounced in 30 days” both read as a condition on Bounced Email.


Pseudo market identifiers (locale)

What we look for: Flow splits, trigger filters, or flow-level profile filters that use locale, country, or related dimensions (for example Customer Locale on the event, or location['country'] on the profile)—often a stand-in for market in multi-market shops.

Rules: Success if none; Info if some flows use those signals. Informational only.

Caveats: The next flow sync refreshes stored dimensions; very old flow rows may not list dimensions until Klaviyo reports the flow as updated.


Klaviyo attribution windows

What we look at: The time gap between a Placed Order and the email/SMS touch Klaviyo credited it to, sampled from the last 7 days. Because Klaviyo does not expose account-level attribution settings via API, we infer a window only for channels we actually see in that sample (Email click, Email open, SMS click, SMS open — each omitted if absent). We snap the largest observed gap per channel to the nearest standard bucket — 12 hours, 24 hours, 5 days, or 7 days. Push, WhatsApp, and SMS delivery appear in the banner only when we have samples and the inferred window differs from Klaviyo’s documented new-account default.

Why it matters: Wider attribution windows (especially opens or clicks beyond 24 hours) inflate Klaviyo Attributed Value (KAV) by crediting orders that customers would likely have placed anyway. Switching to 1-day click attribution typically gives a more honest view of incremental email/SMS revenue.

Rules:

Outcome Rule
Success Every observed email/SMS click or open window listed in the banner is ≤ 1 day.
Info Any observed email/SMS click or open in the banner is wider than 1 day (informational only; not Warning/Error). When at least 25 sampled orders sit in the Littledata flow buckets we also estimate the reduction in revenue Klaviyo reports for Littledata flows under 1-day-click attribution with no open credit, phrased as an upper bound (“could reduce … by up to N%, based on M sampled orders”). Below that floor the banner states the window and nothing else — no impact figure.
Unknown Fewer than ~100 sampled Placed Orders across the 7-day window, or enough orders but no email/SMS click/open touches appeared in the sample (so we cannot list those windows).

Banner wording: Results read like Email 1-day click, 1-day open and SMS 12-hour click, 1-hour open (windows use N-day / N-hour; each channel is one clause).

Tooltip (Read more): A per-flow breakdown of Littledata-flow revenue under the current attribution model versus a 1-day-click model, one line per Littledata flow with the percentage delta and the sampled order count behind it. A flow with fewer than 5 sampled orders shows the count in place of a percentage (2 sampled orders — too few to compare). Assumes Klaviyo’s default cooperative last-touch model (linear is only available on Advanced KDP / Marketing Analytics plans).

Caveats: Channels with no matching touch in the sampled orders are left out of the banner (we do not substitute Klaviyo’s account defaults). Accounts that only send email may therefore show email lines only; that does not mean SMS is set to 1-day. The estimate uses sampled orders (capped at 200/day), not the full revenue picture, so the % reduction is directional rather than exact.

Why the estimate is an upper bound: The snapshots record only the last touch Klaviyo credited. An order the current model credits to an open may also have had a click within the day that would keep it attributed under a 1-day-click model, so the true reduction is at most the figure quoted. Two floors keep the figure from being read as precise: the 25-order account floor above (which also gates the “under 10%, so Success” tolerance — a thin sample cannot flip the verdict in either direction), and the 5-order per-flow floor in the tooltip. When the figure is withheld, details.kavReductionOrderSample and an insignificanceReason of count_too_small record why, while the raw kavReductionPct is still stored for internal use.


What we look at: For each sampled Shopify customer, their email and SMS marketing consent in Shopify versus the same identity’s consent in Klaviyo. This is the only Klaviyo check about compliance rather than performance: if Shopify records that someone unsubscribed and Klaviyo will still send to them, that person keeps receiving marketing they withdrew consent for.

Why it matters: Consent lives in two systems that sync imperfectly, and Klaviyo deduplicates identities imperfectly, so one person with several email addresses can end up as several profiles carrying different consent. Neither system flags the disagreement on its own.

Three states, deliberately:

State Meaning
In sync Shopify and Klaviyo agree, or disagree in a way with no consequence.
Definitely out of sync We are certain they disagree, in a direction that matters.
Not sure We could not confidently match the two systems, so we draw no conclusion.

The decision table

“Will Klaviyo send?” comes from Klaviyo’s own can_receive_email_marketing / can_receive_sms_marketing, which accounts for suppressions — a bounced profile can read SUBSCRIBED and never be sent to.

Shopify Klaviyo Verdict
UNSUBSCRIBED (and more recent) will send Definitely out of sync — critical. Marketing without consent.
UNSUBSCRIBED, but Klaviyo consent is newer will send Not sure — they re-subscribed in Klaviyo after opting out; Shopify is the stale side
UNSUBSCRIBED won’t send In sync
PENDING (double opt-in never confirmed) will send Unconfirmed opt-in — reported, never scored. See below.
PENDING, but Klaviyo holds its own confirmation will send Not sure — see below
PENDING won’t send In sync
SUBSCRIBED will send In sync
SUBSCRIBED won’t send Out of sync (reverse) — lost reach, not a breach. When this is email, the check also estimates missed revenue (see below).
NOT_SUBSCRIBED will send Not sure — usually a legitimate Klaviyo popup signup Shopify never saw
NOT_SUBSCRIBED won’t send In sync
INVALID / REDACTED any Not sure — Shopify’s own state says nothing

PENDING is a consent answer, not a missing one — but it is reported, never scored. Shopify is stating that the person entered an address and never confirmed it, so consent was not completed, and on a store running Shopify’s double opt-in this is often the largest single population (Revendo: 1,012 of 4,993 sampled). Filing it under “not sure” hid a real question; scoring it would be worse. On most stores Klaviyo, not Shopify, is where consent is actually collected — Shopify’s PENDING is then a stale artefact of a confirmation email the merchant never intended to rely on, and turning the check red on it would redden stores doing nothing wrong. So it gets its own verdict, divergedUnconfirmed, surfaced with the system-of-record context that says how much weight to give it, and kept out of every threshold.

Two escape hatches keep it honest, since Klaviyo can legitimately hold a confirmation Shopify never saw:

Consent recency matters. A customer can opt out in Shopify and then opt back in through a Klaviyo form. Comparing values alone would call that a breach. We compare Klaviyo’s consent_timestamp against Shopify’s marketingUpdatedAt and only count it as a breach when Shopify’s opt-out is the more recent event. When Klaviyo has no timestamp we take the conservative reading and count it.

Why “not sure” happens (each points at a different fix):

Never compared at all: customers with no email or phone on that channel. This is neither a sync problem nor a consent problem — there was nothing to compare — and it is usually the biggest number on the page, since two thirds of customers typically have no phone. Those rows never enter compared, so they cannot dilute a rate or inflate the “not sure” headline; they are carried only as noIdentityOnChannel so a run’s arithmetic against sampled still adds up. (Cocopup’s SMS channel first reported 1,365 “not sure” of which 1,328 were simply customers with no phone number.)

consentSystemOfRecord is inferred from which side holds opt-ins the other never saw, read on email only — SMS reverse divergence is dominated by stores that simply never collected SMS consent in Klaviyo and would swamp the signal (Brecks: 893 reverse SMS against 85 reverse email).

Signal Meaning
klaviyoOnlyOptIn Klaviyo subscribed, Shopify has no record → signup happened in Klaviyo
divergedReverse Shopify subscribed, Klaviyo not → consent captured at Shopify, never reached Klaviyo

A verdict needs at least 20 unmatched opt-ins and a 3:1 lean; otherwise unclear. Measured live:

Store Klaviyo-only Shopify-only Verdict
Revendo 1,204 15 klaviyo
Cocopup 302 37 klaviyo
Brecks 3 85 shopify

This never changes the status. It exists so the rest of the snapshot reads correctly: on a Klaviyo-led store a large Shopify PENDING or NOT_SUBSCRIBED population is the design, not a fault.

⚠️ It measures where consent is collected, not where email is sent. Detecting whether Shopify Email is actively sending would settle it, and there is currently no way to do so: marketingActivities needs read_marketing_events and appInstallations needs read_apps (both denied on our token); openTrackingLevel reads UNKNOWN for 100% of customers on every store tested, with no positive control available; and BuiltWith’s “Shopify Email Marketing” tag appears on 75.5% of all Shopify stores, so it detects the capability rather than its use.

Missed revenue from unsynced Shopify opt-ins

When Shopify records that someone granted email consent but Klaviyo will not send to them, the check prices the gap as an uplift:

(unsynced email opt-ins ÷ email profiles compared) × everything Klaviyo earned this account in the last 30 days.

If 2% of the profiles Klaviyo could be sending to are missing from it, Klaviyo is making at most 2% less than it would with them. The denominator is flows + campaigns: flow revenue is already stored per flow in USD by the revenue sync, and campaign revenue is fetched as one account-level total from campaign-values-reports and converted (klaviyoCampaignRevenue30d is null, not zero, when that report cannot be read, so a partial denominator is visible in the details).

Why not revenue per recipient. The first version multiplied the opt-in backlog by last month’s attributed revenue per email recipient — but a recipient is one message delivered, not one person. Measured across 331 live accounts, that produced a figure larger than the account’s entire measured Klaviyo revenue on 40 of them, topping out at 7,988% (strong-fitness-de: $18,335 “missed” against $230 of flow revenue). The uplift model is bounded by construction — missed dollars can never exceed missed profiles as a share of the list — and zero accounts now exceed 100%.

It appears in the check message as “syncing that consent would add about $X a month in revenue” when the unsynced opt-ins are the headline; beside a compliance finding the message names only their count (“Another N opted in to email in Shopify but aren’t subscribed in Klaviyo.”). It does not change the status. We omit it when there are fewer than 3 unsynced opt-ins, fewer than 50 email profiles compared, the account has no measured Klaviyo revenue, or the figure is under $1.

Materiality is one threshold, not two: 0.5%. Because the dollars are the profile share, clearing 0.5% of revenue is clearing 0.5% of the list. Under that, a merchant is being asked to fix a sync over less than half a percent more sending. Persisted as unsyncedShareOfProfiles. When it is suppressed but unsynced opt-ins exist, the message is “No one is being marketed to without consent.” — never the stronger “both systems agree on every customer”, which would be untrue.

Across the estate (459 accounts checked, 331 with a computable figure): 304 clear the 0.5% floor, median 3.4% of profiles missing, median $234/month flows-only.

⚠️ Klaviyo does not set consent: "UNSUBSCRIBED" when someone clicks the unsubscribe link. Consent stays exactly where it was, can_receive_email_marketing flips to false, and an entry appears in subscriptions.email.marketing.suppression[] with a reason. Hard bounces and spam complaints behave the same way. A real profile from Love Brand & Co:

"consent": "SUBSCRIBED",                    // unchanged
"can_receive_email_marketing": false,       // will not be sent to
"consent_timestamp": "2026-08-15T16:24",    // signed up
"suppression": [{ "reason": "USER_SUPPRESSED", "timestamp": "2026-08-15T17:37" }]  // left, an hour later

Reading consent alone — or can_receive_* without the reason — cannot tell a dead address from a withdrawal from an opt-in Klaviyo never received. All three look like “Shopify opted in, Klaviyo did not get it”. Estate-wide, 91,182 of 158,895 reverse divergences (57%, across 421 shops) were profiles Klaviyo already held consent for. On the account that prompted the investigation, 2,786 of 2,794.

DIVERGED_SUPPRESSED keeps them out of divergedReverse and out of the missed-revenue estimate, and tallies Klaviyo’s reasons in suppressionReasons so the summary can say which problem it is:

Suppression Meaning Beats a newer Shopify opt-in?
HARD_BOUNCE, INVALID_EMAIL, SPAM_COMPLAINT The address is the problem Yes — no consent record makes a dead mailbox deliver
USER_SUPPRESSED, MANUAL_SUPPRESSION The person left No — a later Shopify opt-in is them coming back, and is genuine lost reach

Neither is a consent-sync fault, and neither is repaired by pushing consent again. A wall of hard bounces is a list-hygiene finding; a wall of user suppressions is people leaving.

When Klaviyo holds the newer opt-out

Shopify SUBSCRIBED + Klaviyo not-subscribed has two causes, and the check used to treat them as one:

Klaviyo’s record Meaning Verdict
Older than Shopify’s opt-in, or no timestamp Consent captured at Shopify never reached Klaviyo divergedReverse — lost reach, feeds the estimate
UNSUBSCRIBED, newer than Shopify’s opt-in The person withdrew in Klaviyo; Shopify never heard divergedKlaviyoOptOut — never lost reach, never in the estimate

The critical direction has always had this rule (“a Klaviyo opt-in after Shopify’s opt-out means the person came back”); the reverse direction did not, so every Klaviyo unsubscribe against an older Shopify opt-in was counted as revenue we were telling the merchant to go and collect. Syncing that consent back would re-subscribe someone who unsubscribed.

It is still a finding: Shopify holds the stale record, so anything reading Shopify’s consent — Shopify Email, another app — can market to them. Reported in the channel summary and klaviyoOptOutsNotInShopify, not in the status.

⚠️ Only an explicit UNSUBSCRIBED counts. NEVER_SUBSCRIBED is the absence of a consent event and its timestamp falls back to last_updated, which moves on any profile edit — without that gate, every recently-touched profile would read as a withdrawal and the reverse count would empty out.

Found counts, and what goes in the message

Every count in a message is found, not estimated: we compared N customers and this many diverged. The message says so — “66 sampled customers…” — because the list is far larger than any one run, so a bare count reads as the whole problem when it is a floor.

The message carries the finding and nothing else: the count, the strict-consent countries when they are what tripped it, and the dollars when unsynced opt-ins are the headline. The share of customers, the coverage of the sample (cumulative distinct identities over the trailing 30 days) and the store total stay in details (sampled, cumulativeIdentitiesChecked, storeCustomerTotal, the per-channel counts and summary). There is no furtherInfo: the comparison method and sample coverage are methodology, not something the merchant acts on. The store-wide projection the reverse-divergence message used to quote (“…across ~230,000 customers”) is gone for the same reason.

Three gates before the check goes red

A critical divergence must pass all three:

  1. Stable — Shopify’s opt-out is more than 24 hours old, so an in-flight sync cannot explain it.
  2. Confirmed — the same identity was already critical at its previous check, so Klaviyo’s sync has had a full check cycle to catch up. A store’s first run can never report Info; it resolves to Success with a message saying so.
  3. Material — clears the tolerance for its jurisdiction, and there are at least 3 of them.

The check never goes above Info: a consent mismatch is between the merchant’s Shopify and Klaviyo accounts, not something Littledata can fix for them today, so it is surfaced without alarming.

Outcome Rule
Info ≥3 confirmed criticals and either >1% of all matched identities, or >0.2% of matched identities in strict-consent countries. Also >5% reverse divergence on a channel.
(never) Unconfirmed double opt-ins, however many. Reported in the summary only.
Success Everything below those thresholds, including within-tolerance stable criticals (still described in the message).
Unknown Fewer than 50 matched identities on both channels, no Shopify token, or the app lacks read_customers.

Why a tolerance at all: the comparison is imperfect at the margin — a customer merged in Shopify, an address changed on one side, a profile updated between our two API calls. A check that flags a single ambiguous row trains people to ignore it. Everything under the tolerance is still reported in the message, as success rather than silence.

Why Europe is five times tighter: in Germany, under the UWG, an unsolicited commercial email is an actionable unfair-competition tort that any competitor or consumer association can pursue by Abmahnung — no regulator, no volume threshold, one message is enough. The rest of the EU/EEA, the UK and Switzerland carry the same duty under GDPR Art. 7(3) and ePrivacy/PECR; Canada’s CASL is comparable. The US is excluded: CAN-SPAM gives a 10-business-day window to process an opt-out and requires no prior consent, so a short sync gap there is a different kind of problem.

Sampling

5,000 distinct customers per run, most-recently-updated first, deduplicated by customer ID, allocated in four passes:

  1. Store-wide, 1,000 — the general health signal.
  2. Germany — takes as much of the remaining 4,000 as it can fill. A US-heavy store would otherwise contribute a handful of German rows, far too few to tell a pattern from noise.
  3. Rest of the EU/EEA/UK/CH/CA — whatever Germany left unfilled.
  4. Store-wide again — spends any budget the jurisdiction passes could not use, so a store selling mostly outside Europe still gets a full 5,000 rather than a truncated sample. (Brecks has 3M customers but only ~220 in strict-consent countries; without this pass it sampled 1,345.)

The 5,000 is a ceiling on distinct customers, not a per-pass limit — later passes fetch more rows than they need because most of what they return is already in the sample and dedups away.

⚠️ Shopify’s country: filter matches the country name, not the ISO code, despite the docs saying either works. country:DE returns zero rows — silently, looking exactly like “this store has no German customers”. Verified against a live store on 2026-08-13. The match is also fuzzy (country:Germany returned a Swiss customer), so the filter only biases the sample; every row’s real country is read from defaultAddress.countryCodeV2 afterwards, and a unit test asserts no query term is ever an ISO code.

Recency ordering is a biased sample by design: a consent change bumps updatedAt, so the customers most likely to have diverged are seen first. Rates therefore describe recently active customers, not the whole list, and the check’s wording says so. The strict-jurisdiction rate is measured against its own denominator, not the global one. Coverage of the older tail accumulates across daily runs, reported as cumulative distinct identities checked over the trailing 30 days.

Identity quality (informational)

Reported in details.identityQuality, never affecting the status. It sizes how badly Klaviyo’s identity resolution behaves — the thing that makes the “not sure” bucket large — and aggregates across accounts to size the problem estate-wide:

Metric Failure mode
emailsWithMultipleKlaviyoProfiles Splitting — consent set on one profile does not apply to the others
externalIdsWithMultipleKlaviyoProfiles Splitting, by Shopify customer ID
klaviyoProfilesMatchedByMultipleCustomers Merging — several addresses flattened onto one profile
customersWithConflictingProfiles Email and external_id point at entirely different profiles
shopifyCustomersSharingEmail Shopify’s own duplicates, before Klaviyo is involved
cleanMatchRate Headline: share resolving to exactly one profile nothing else claims

Drill-down

details.affectedKlaviyoProfileUrls gives up to 50 direct klaviyo.com/profile/{id} links. Klaviyo has no URL for an ad-hoc set of profile IDs, and its segment builder cannot reference Shopify, so no segment can express “diverged from Shopify’s record” — the divergence only exists once the two systems are compared outside Klaviyo. The alternative would be writing a littledata_consent_divergence property onto the affected profiles so a segment can target them; that is a write into the merchant’s account and needs their explicit agreement first.

Privacy

No email address or phone number is ever written to the database. Identities are stored only as sha256(pepper + shopName + channel + identity), shop-scoped (the same address at two stores cannot be correlated) and peppered via CONSENT_HASH_PEPPER (not reversible from a list of known addresses). The cache exists to confirm divergences across runs and to track cumulative coverage, and expires after 30 days. The only person-level pointer persisted is the Klaviyo profile ID of a diverged profile.

Database cost

The check touches two collections, klaviyoConsentDivergenceSnapshots (one counts-only document per shop per UTC day) and klaviyoConsentIdentityCache (one hashed row per compared identity, up to 2 × 5,000 per shop). Three things keep that from hurting the database:

Caveats

Reads Shopify’s defaultEmailAddress — a customer has one default address, so one person with several emails appears as several customer records, compared separately (which is also how consent works legally). SMS consent reads Shopify’s deprecated defaultPhoneNumber.marketingState, currently the only field exposing it. Country comes from defaultAddress.countryCodeV2; customers with no address are bucketed unknown and never treated as strict, so a missing address never fires the check. Consent held in Klaviyo but not Shopify is never a breach, so a store collecting consent purely through Klaviyo forms shows a large klaviyoOnlyOptIn count and no error. Requires read_customers; without it the check is Unknown, never Success.

Preview: npx tsx scripts/preview-consent-divergence.ts <shop.myshopify.com> dry-runs a store and prints the full breakdown without writing anything. A dry run behaves like a first run, so it never shows an escalation the live check would not yet produce.


Spam profiles created

What we look for: whether someone is injecting machine-generated profiles into the account. This is a data-quality check, not a deliverability one: the invented addresses do not exist, so the first flow email to reach them hard-bounces and the sending domain wears the damage. On the account this was built from, one flow message took 2,373 hard bounces in five days and its 30-day bounce rate read 61%.

What we read: the 100 newest profiles, in one API request, never paginated. Injection is contiguous in time — a generator works through a queue — so a newest-first page either contains the flood or the account is quiet. The cost is therefore identical for a five-million-profile account and a five-thousand-profile one.

Signals

Two tiers. Primary signals are hard to explain innocently; corroborating ones only support them.

Signal Tier Measured separation
Share of profiles on cloud-server IPs (AWS + GCP published ranges) Primary ≥25% only alongside address evidence (or ≥50% alone), corroborating ≥10% 0% of profiles that placed an order, 4.9% of ordinary bouncers, 45.7% of a flood cohort
Addresses following a generated name template (firstname.lastname4137@) Primary ≥40%, corroborating ≥20% 0.5% of real buyers, ~100% inside a flood
Addresses spread too evenly over a few free providers Primary 7 providers at near-equal share, gmail under-represented; organic traffic is dominated by one provider in every market sampled
A block of ≥25 generated addresses that on their own look drawn from a pool, or run from cloud servers Primary catches a flood diluted by real signups, which pushes every whole-page share below its threshold
Never opted in to marketing Corroborating ≥60%  
Created inside one clock hour Corroborating ≥25% only counted when the page spans ≥6 hours

The provider-pool clause says what to expect instead. “Addresses spread evenly over 9 free providers” left the reader to work out why that was bad; it now reads “spread too evenly … none holding more than 40% — a real list is dominated by one provider, usually Gmail”. The abnormality is the flatness: a generator draws from its pool uniformly, where a real list skews hard to one provider. Gmail is named as the usual case rather than the rule, because the threshold is deliberately country-agnostic — absolute Gmail share moves with the market, but “no provider dominates” holds nowhere organically.

Every clause names a count, not a share — “35 of the 100 newest profiles use formulaic-looking addresses” is something a merchant can go and look at, where a percentage of a page they cannot see is not. Thresholds stay as shares; only the copy changed.

Rules:

Outcome Rule
Error ≥2 primary signals and ≥2 corroborating — a proven injection.
Warning Any single primary signal.
Info A firing verdict on a store whose klaviyo connection has bot filtering (Connection.settings.filterUnidentifiedEvents) enabled — bot profiles are still landing in the account (Klaviyo’s own Shopify sync creates them), but Littledata’s triggers filter them, so Littledata-triggered flows never email them.
Success No primary signal.
Unknown Fewer than 50 profiles in the account, or Klaviyo’s profiles API could not be read.

Corroborators alone never flag (a fleet sweep showed every corroborator-only flag was a false positive); a diluted flood is caught by the generated-address sub-cohort primary instead. The Shopify-side sweep that calibrated this grading is described in Shopify audit checks.

No single threshold can move this check — independent signals have to agree. That is deliberate: the retired klaviyoFlowDeliverability check fired on one noisy number, and Klaviyo deliverability is consistently good, so a bare rate told nobody anything.

Error and Warning are correct severities here because Littledata can actually fix it — Bot Protection filters these profiles out of Littledata’s triggers, and both checks link to its help article. The usual rule of capping third-party findings at Info does not apply to a finding the product resolves.

The message states the scale and the action; there is no furtherInfo. The message says how many of the newest profiles look fake and offers Bot Protection — e.g. “68 of the 100 newest profiles look fake, so the first email to each will hard-bounce. Turn on Bot Protection to filter checkout bots from Littledata’s triggers.” It used to open with the primary signal clauses (#644 made the same change to the Shopify check), and then listed every signal as a furtherInfo bullet. The signals are evidence, not items the merchant fixes one by one, so they now stay in details.primarySignals / corroboratingSignals for admins.

The message does not name a route — it says the profiles “look fake”, not where they came from. Deliberate: nothing in a Klaviyo profile records where it came from, so this check has no source evidence of its own. Every instance measured so far arrived through Shopify checkout (a bot creates checkouts, Klaviyo’s own Shopify sync turns them into profiles), but a flood arriving through a signup form or the Klaviyo API would look identical here, so the message says what the check can see and no more. The remedy sentence still names checkout bots — that is a description of what Bot Protection filters, not a claim about where these particular profiles came from. If we ever want the route asserted, condition it on the sibling shopifyCheckoutSpam row having fired for the same shop.

How the message counts “suspicious”. Per profile: a formulaic address or a cloud-server IP nominates one. Neither is proof alone — real people own first.last12@ addresses, and link-scanning proxies put a trickle of genuine subscribers on cloud IPs — which is why the copy says the profiles look fake (or may be bots on a Warning) and the verdict still needs signals to agree. When the whole page’s provider mix reads as drawn from a pool, the page itself is the cohort: a generator that does not use name templates shows up there and nowhere else, so counting templated addresses alone would report zero on a firing verdict.

No unconditional provider-pool line. The old furtherInfo used to append one whatever the verdict, which failed in both directions: on a healthy account it rendered “Addresses sit on 4 email providers, the largest holding 78% of them” under a warning bullet — an ordinary distribution dressed as evidence, the same fault that retired the billing-regions line on the Shopify check — and on a firing one it restated the pool signal that had just convicted in the bullet above it. The top-provider share now sits inside the clause that convicts on it.

Guards against false positives

All were found by measuring real accounts, not by reasoning:

Caveats

Klaviyo overwrites location.ip as later events arrive, so the cloud-IP share is a floor in one direction and inflated in the other: a scanned mail click files a genuine subscriber under AWS. That is why the whole-page share needs address evidence beside it (see above) — on this data source it measures where a profile was last seen, not where it signed up. Only AWS and GCP publish usable prefix lists — Azure has no stable unauthenticated JSON endpoint, and budget hosts favoured by cheaper bots are only identifiable by ASN — so a low cloud share is not evidence of clean traffic, which is why nothing fires on it alone. When no range snapshot is available the check still runs on the identity signals and records datacentreRangesAvailable: false.

Runs in the shared audit phase, so prospects get it too — it is the evidence behind telling a prospect their list is being poisoned, computed from their own account.

No Linear issues. This check and shopifyCheckoutSpam describe the same incident from two sides; only the Shopify check opens the Linear issue (it covers every customer with the orders scope, whatever ESP they use), so this one is in linearSuppressedChecks. It stays fully visible to customers, prospects, Slack and marketing automations — it just never doubles the investigation.

Its Shopify counterpart (Bot checkouts) sees the same problem at the source and fires whatever the merchant sends email with — see the Shopify audit checks.