Duplicate payment events
Duplicate Payment Events: Root Cause and Fix
Admaxxer is a DTC analytics platform with built-in Meta + Google ad ops. If a purchase fires from both the client-side pixel and the server-side Shopify webhook without a shared `event_id`, Admaxxer counts the revenue twice and every downstream metric — MER, ROAS, LTV, MMM contribution — goes wrong in the same direction. TL;DR: pick one source of truth, emit a deterministic `event_id` (the Shopify order id works great), and deduplicate at ingest.
## Symptoms
- Revenue in Admaxxer is roughly 2x what Shopify reports for the same day.
- Blended MER looks unrealistically good.
- CAPI match rate looks fine but your purchase count is inflated.
- The event log contains two rows per order — one with `source = pixel`, one with `source = shopify_webhook`.
- Cohort LTV curves shift up right after Shopify was connected or the pixel was installed.
## Root cause
Admaxxer supports multiple purchase sources because different stacks need different combinations:
- Client-side pixel `purchase` event.
- Server-side Shopify `orders/paid` webhook.
- Server-side CAPI event for Meta.
If two of those fire for the same order and the `event_id` is different, Admaxxer treats them as distinct events. The common mistakes:
1. Firing `admaxxer.track('purchase', ...)` on thank-you page AND having Shopify webhook subscribed to `orders/paid`.
2. Using a random `event_id` (uuid per beacon) instead of the order id.
3. Sending CAPI events with the same `event_id` but different `event_time` such that the deduplication window is exceeded.
## Fix
### Step 1: Decide on one source of truth
For Shopify brands, the webhook is authoritative because it survives ad blockers and client-side bounces. Keep the pixel `purchase` firing only if you need CAPI match-rate signal, and make sure it sends the same `event_id`.
### Step 2: Emit a deterministic event_id
On the pixel side, pass the Shopify order id (or your checkout's internal order id) as the `event_id`:
```js
admaxxer('track', 'purchase', {
event_id: 'shopify_' + orderId,
value: total,
currency: 'USD',
order_id: orderId
});
```
The webhook should emit the same `event_id` format. Admaxxer deduplicates on `event_id`.
### Step 3: Backfill deduplicate
If doubles already exist, run the Admaxxer "purge duplicate purchase events" job from the Admin panel, or contact support. Do not manually delete rows — it breaks cohort backfills and LTV recomputation.
### Step 4: Audit other event types
Duplicates usually hit `purchase`, but `add_to_cart` and `initiate_checkout` can get doubled the same way. Use the same `event_id` strategy across all ecommerce events.
### Step 5: Verify CAPI parity
If you send CAPI events to Meta separately, use the same `event_id` there too. Meta deduplicates on `event_id` + `event_name` within a window — diverging ids break CAPI match rate and Meta's delivery system will treat the same order as two separate conversions.
### Step 6: Handle multi-subscription and installment orders
Some stacks split an order across multiple `orders/paid` events (installment plans, deferred shipping, subscription renewals). Use the parent order id plus a suffix, e.g. `shopify__charge_`, so each charge is unique but traceable back to the parent order. Without the suffix, renewals silently deduplicate against the original order and revenue is undercounted.
## Verify the fix
- Today's Admaxxer revenue matches Shopify gross revenue within rounding error.
- Each order id appears exactly once in the Admaxxer events log.
- Blended MER settles to a realistic number.
- Meta Events Manager's "Deduplication" tab shows a healthy server-vs-browser match, not duplicates.
- Subscription renewals and installment charges appear as separate events with distinct suffixes.
## Prevent it next time
- **Make `event_id` deterministic from day one.** Always derive from the order id or a stable server-assigned id.
- **Single source of truth.** Prefer server-side for authoritative revenue; treat pixel as a signal for attribution, not counting.
- **Monitor duplicate rate.** Admaxxer can alert when the same `event_id` arrives from two sources more than N times an hour.
- **Write an E2E test.** A headless purchase test that asserts exactly one purchase row catches regressions before they hit production.
## Related guides
- [Shopify orders webhook not firing](/guides/shopify-orders-webhook-not-firing)
- [Verify revenue attribution after install](/guides/verify-revenue-attribution-after-install)
- [Troubleshoot duplicate events](/documentation/troubleshoot/duplicate-events)
## FAQs
**Q: Should I turn off the pixel purchase event entirely?**
A: Only if you do not care about CAPI match rate. Keeping the pixel purchase is useful for Meta CAPI parity as long as the `event_id` is shared with the webhook.
**Q: What about partial refunds — do those create duplicates?**
A: No. Refunds fire on `orders/refunded`, not `orders/paid`, so they get their own `event_id` suffix (e.g. `shopify_refund_`) and do not double-count.
**Q: How does deduplication work across Admaxxer, Meta CAPI, and Google Ads Enhanced Conversions?**
A: Each platform deduplicates within its own window. Using a consistent `event_id` across all three minimizes both under- and over-counting.
Frequently Asked Questions
Should I turn off the pixel purchase event entirely?
Only if you do not care about CAPI match rate. Keeping the pixel purchase is useful for Meta CAPI parity as long as the event_id is shared with the webhook.
What about partial refunds — do those create duplicates?
No. Refunds fire on orders/refunded with a distinct event_id suffix, so they do not double-count paid orders.
How does deduplication work across Admaxxer, Meta CAPI, and Google Ads Enhanced Conversions?
Each platform deduplicates within its own window. A consistent event_id across all three minimizes both under- and over-counting.
Put This Knowledge Into Action
Bring Meta and Google ads into one self-hosted workspace.
Get Started Free