TL;DR: Duplicate revenue almost always comes from two sources firing the same conversion — typically a client-side purchase event on the "thank you" page AND a Stripe webhook. The fix is to pass the same admx_event_id to both ingestion paths; Admaxxer will dedupe within a 24-hour window.
Every event you send can carry a unique admx_event_id (formerly dfid_event_id; the admx_ prefix is the current name). When two events arrive within 24 hours with the same admx_event_id, the second one is dropped at ingest. If you do not pass an ID, we generate one per request — which means two network paths firing for the same real-world purchase will not dedupe.
The most frequent pattern. Your checkout flow fires a client-side purchase event on the "thank you" page, and your Stripe webhook also posts a purchase to /api/event. Two events, same money — your dashboard shows 2x revenue.
Fix: have both sides emit the same admx_event_id. Use the Stripe payment_intent.id or checkout.session.id — it is stable across both paths.
// client-side (thank-you page)
window.admx('purchase', {
admx_event_id: '{{ stripe_payment_intent_id }}',
amount: 49.00,
currency: 'USD',
});
// server-side (Stripe webhook handler)
await fetch('https://admaxxer.com/api/event', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
name: 'purchase',
admx_event_id: paymentIntent.id, // <-- same ID
amount: paymentIntent.amount / 100,
currency: paymentIntent.currency,
website_id: 'your_site_id',
}),
});
Stripe retries webhooks that do not return 2xx within a few seconds. If your handler is slow (e.g. calls an LLM, renders a PDF) Stripe may retry while your first handler is still running — both end up POSTing to Admaxxer.
Fix: return 200 immediately from the webhook endpoint and process asynchronously. Use the Stripe Event.id (not payment_intent.id) as admx_event_id on the async path — it is unique per delivery attempt, so Stripe retries won't double-record.
app.post('/stripe-webhook', express.raw({type:'application/json'}), async (req, res) => {
res.status(200).end(); // ACK first
const event = stripe.webhooks.constructEvent(req.body, sig, secret);
await queue.add('forward-to-admaxxer', {
eventId: event.id, // <-- dedupe key
paymentIntent: event.data.object,
});
});
If you call window.admx('purchase', ...) inside a useEffect without a dependency array, React Strict Mode (dev) or component remounts (prod) can fire it twice.
Fix: guard against remount with a ref, or move the call to a server component / route loader. And always pass admx_event_id.
const fired = useRef(false);
useEffect(() => {
if (fired.current) return;
fired.current = true;
window.admx('purchase', {
admx_event_id: props.orderId,
amount: props.total,
});
}, []);
purchase.admx_event_id — our dedup should have caught it. Contact support.admx_event_ids — it is a code-level duplicate. Pick one path (we recommend the server-side webhook) and remove the other, or pass a shared ID as above.Admaxxer treats events as append-only for audit integrity, but you can mark a dupe as "excluded" via the API:
curl -X POST https://admaxxer.com/api/v1/pixel/events/exclude \
-H 'Authorization: Bearer YOUR_API_KEY' \
-H 'Content-Type: application/json' \
-d '{"admx_event_id": "pi_3PxxxYYY", "reason": "duplicate"}'
Excluded events do not count toward revenue, conversions, or goals, but remain visible (greyed out) in the event log.
admx_event_id. Even for non-purchase events — a stable ID per conversion is free insurance against any future duplicate path.revenue with a z-score threshold of 3 — duplicates usually trip it.