Fixing Duplicate Payment Events in Admaxxer

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.

How Admaxxer deduplicates

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 three common causes

1. Client "purchase" event + Stripe webhook

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',
  }),
});

2. Webhook endpoint retries

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,
  });
});

3. Double-mounted React / Vue components

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,
  });
}, []);

Diagnostic checklist

  1. Open the Admaxxer dashboard, go to Analytics › Events, filter to purchase.
  2. Look for two rows with the same amount, same user, within seconds of each other.
  3. If they share an admx_event_id — our dedup should have caught it. Contact support.
  4. If they have different 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.

Deleting duplicates you already recorded

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.

Prevention going forward

Related

CSP errors · Pixel API Endpoints · Pixel API Errors