Controleer eerst één Purchase, zonder iets te wijzigen
Lijkt Meta een aankoop twee keer te tellen? Begin met één bekende aankoop en de events die al beschikbaar zijn in Events Manager of je integratielogs. Schakel geen werkende integratie uit en plaats geen nieuwe betaalde bestelling alleen voor deze controle.
- Noteer de bronnen. Welke native kanalen, apps en GTM-containers versturen
Purchase, en naar welke dataset? - Vergelijk één paar. Noteer voor dezelfde aankoop de browser-
eventID, server-event_iden eventnamen. Geef per veld aan: gelijk, verschillend of niet beschikbaar. Raad ontbrekende waarden niet. - Scheid observatie en diagnose. Twee ontvangen records of een verschil tussen de totalen van Meta en Shopify bewijzen op zichzelf niet dat een aankoop dubbel is geteld. Bewaar de eventdetails en de periode voor verder onderzoek.
Verschillen de waarden of verbergt een app ze? Bereid een aanvraag voor een Meta Purchase-controle voor. Zo neem je deze observaties mee, zonder klantgegevens te versturen of eerst de configuratie te wijzigen.
Hoe koppelt Meta een browser-event aan zijn server-tweeling?
De Conversions API van Meta is ontworpen om naast de browser-Pixel te draaien, niet in plaats ervan. Beide zijn bedoeld om dezelfde actie in de echte wereld te beschrijven vanuit twee gezichtspunten: de Pixel vuurt af vanuit de browser van de koper, de Conversions API stuurt hetzelfde event vanaf je server.
Beide versturen is de aanbevolen opzet, omdat elk de blinde vlekken van de ander dekt — de browser vangt events die een server zou kunnen missen, de server vangt events die een adblocker of een afgeknotte cookie in de browser om zeep helpen.
De adder onder het gras is dat Meta nu twee registraties heeft van één purchase. Deduplicatie is het mechanisme dat ze weer tot één samensmelt. Als het werkt, krijg je de dekking van twee bronnen en het aantal van één. Als het faalt, krijg je het aantal van twee, en elk cijfer stroomafwaarts — conversievolume, kosten per purchase, ROAS — klopt niet, en wel in de richting die slechte campagnes er goed laat uitzien.
Deduplicatie is geen automatische magie. Het is een match op sleutels die jij correct moet instellen.
Waarop matcht Meta om Pixel- en CAPI-events te dedupliceren?
De primaire deduplicatiemethode van Meta vergelijkt twee velden tussen het browser- en het server-event:
- event_id — een unieke identifier voor het specifieke event. In de Pixel wordt hij doorgegeven als
eventID; in de Conversions API-payload is het het veldevent_id. Dezelfde waarde, twee verschillende schrijfwijzen. - event_name — het standaard event, bijvoorbeeld
Purchase. Heteventvan de Pixel moet gelijk zijn aan deevent_namevan de Conversions API.
Als een browser-event en een server-event dezelfde event_id en dezelfde event_name delen, en Meta het tweede binnen 48 uur na het eerste ontvangt, worden ze als één event behandeld. Volgens de documentatie van Meta gebeurt matching op event id plus event name samen — het ene zonder het andere telt niet.
Dit is wat er in elk faalgeval daadwerkelijk gebeurt:
- event_id verschilt tussen de twee bronnen. Geen match. Meta registreert twee conversies. Eén purchase wordt er twee.
- event_id wordt bij elke paginalaad opnieuw gegenereerd. Browser en server dragen nooit tegelijk dezelfde waarde, dus niets matcht. Dit is de meest voorkomende zelfveroorzaakte bug.
- event_id ontbreekt aan één kant. Niets om tegen te matchen. Beide tellen mee.
- event_name verschilt (
Purchaseversuspurchase). Id’s kunnen perfect matchen en toch faalt het, omdat de naam deel uitmaakt van de sleutel. - Het tweede event komt na 48 uur binnen. Buiten het venster heeft Meta het eerste al afgerond; de late tweeling telt op zichzelf mee.
Het zakelijke gevolg heeft altijd dezelfde vorm: opgeblazen conversieaantallen voeden een opgeblazen ROAS. Een campagne die een ROAS van 4,0 rapporteert terwijl hij dubbel telt, draait in werkelijkheid op 2,0. Je schaalt hem op omdat het dashboard zegt dat hij wint, en je pompt budget in een campagne die hoogstens quitte speelt. Dubbele events voegen niet alleen ruis toe — ze belonen actief je slechtste beslissingen.
Waarom vuurt Shopify Meta Purchase-events dubbel af?
Op Shopify komt duplicatie zelden van één verkeerd geconfigureerde tag. Het komt van twee systemen die allebei denken dat ze eigenaar zijn van het Purchase-event, zonder van elkaar te weten. De terugkerende combinaties:
- Native Facebook & Instagram-kanaal + een CAPI via GTM of app. Het native kanaal verstuurt zijn eigen browser- en server-events. Voeg een tweede server-route toe via GTM of een app en je hebt nu twee Purchase-events per bestelling zonder gedeelde id.
- Een CAPI-app + de thema-/Customer Events-Pixel. De app verstuurt server-side Purchase, de storefront-Pixel verstuurt browser-Purchase, en de app genereert zijn eigen id’s die de Pixel nooit ziet.
- Twee CAPI-apps tegelijk geïnstalleerd — vaak een overblijfsel van een eerdere opzet die nooit werd verwijderd. Beide vuren af; geen van beide coördineert.
- checkout.liquid-Pixel + een Customer Events-Pixel tijdens een halfafgeronde migratie naar checkout extensibility, waarbij beide op de bedankpagina overleven.
De rode draad: elke bron kan afzonderlijk correct zijn en de combinatie verdubbelt je data toch, omdat deduplicatie alleen werkt als de bronnen het eens zijn over event_id. Twee brave bronnen die nooit id’s uitwisselen zijn erger dan één, omdat ze compleet lijken terwijl ze stilletjes dubbel tellen. Als je aan het uitzoeken bent welke bronnen actief zijn, doorloopt de Meta CAPI-deduplicatiediagnose de inventarisatie stap voor stap.
Diagnostische beslisboom
Vergelijkingstabel — scrol horizontaal om alle kolommen te bekijken
| Wat je ziet in Events Manager | Waarschijnlijke oorzaak | Wat er gebeurt | Oplossing |
|---|---|---|---|
Twee Purchase-rijen per bestelling, beide “Processed”, geen “Deduplicated”-label | Twee bronnen zonder gedeelde id | event_id ontbreekt of verschilt bij elke bron | Route beide via één id-bron; geef dezelfde waarde door aan Pixel en server |
| Paren matchen in Test Events maar het productieaantal blijft ~2× de bestellingen | event_id opnieuw gegenereerd per paginalaad | Id’s matchen alleen binnen één laadbeurt, niet tussen browser en server | Leid event_id af van het order-ID, niet van Date.now() of een nieuwe UUID |
| Server-events tellen apart; browser-events zien er goed uit | event_name-mismatch | Server stuurt purchase/PURCHASE, Pixel stuurt Purchase | Standaardiseer aan beide kanten op Purchase (hoofdlettergevoelig) |
| Sommige bestellingen dedupliceren, andere niet | Race voorbij het 48-uursvenster, of incidentele server-retries met nieuwe id’s | Late of herkeyde tweeling valt buiten de match | Stabiliseer de id; zorg dat de server prompt verstuurt |
| Aantallen kloppen in het aggregaat maar EMQ is laag | Geen dedup-probleem | Dedup is prima; user-data-verrijking is dun | Apart probleem — zie event match quality |
Hoe inspecteer ik deduplicatie in Events Manager?
Je kunt dit niet diagnosticeren vanaf het hoofddashboard — dat toont totalen, en totalen verbergen dubbeltellingen. Ga naar de event-bron en open Test Events:
- Selecteer in Events Manager je dataset (Pixel), open het tabblad Test Events, en kopieer de testcode.
- Trigger een echte testpurchase (of gebruik de test-event-code in het
test_event_code-veld van je server-payload, zodat ook het server-event verschijnt). - Kijk naar de live feed. Een correct gekoppeld event verschijnt met een “Deduplicated”-indicator, en als je het uitklapt zie je dat het van zowel Browser als Server is ontvangen met dezelfde
event_id. - Zie je in plaats daarvan twee losse
Purchase-vermeldingen — één Browser, één Server — zonder dedup-label, dan komen je id’s of namen niet overeen. - Open voor historische data een individueel event in Overview/het eventdetail en controleer de connection method. Een gezonde koppeling rapporteert zowel browser als server; twee onafhankelijke rijen per bestelling is het teken.
Het signaal waar je naar op zoek bent, is letterlijk het label “Deduplicated” op een browser+server-paar. De afwezigheid ervan, terwijl je beide verstuurt, is de bug.
Een schets van een correcte implementatie
De hele oplossing is één idee: genereer de event_id één keer, koppel hem aan de bestelling, en geef exact dezelfde string aan zowel de Pixel als de server. Hier is een technisch correcte vorm.
Browser (Customer Events-pixel of thema), waarbij eventID als vierde argument aan fbq wordt doorgegeven:
// Eén id per purchase, afgeleid van de bestelling zodat hij identiek is op de server.
// NIET Date.now(), NIET een nieuwe crypto.randomUUID() bij elke laadbeurt.
const eventId = `purchase_${order.id}`; // bijv. "purchase_4512890"
fbq('track', 'Purchase', {
value: 129.00,
currency: 'USD',
contents: [{ id: 'SKU-123', quantity: 1 }],
content_type: 'product'
}, { eventID: eventId });
Server (Conversions API-payload), met dezelfde waarde in het snake_case-veld event_id en dezelfde event_name:
{
"data": [
{
"event_name": "Purchase",
"event_time": 1690000000,
"event_id": "purchase_4512890",
"action_source": "website",
"event_source_url": "https://store.example/checkout/thank-you",
"user_data": {
"em": ["<sha256 of lowercased, trimmed email>"],
"fbp": "fb.1.1690000000.1234567890",
"fbc": "fb.1.1690000000.AbCdEfGh"
},
"custom_data": { "value": 129.00, "currency": "USD" }
}
]
}
Drie details doen al het werk: de event_id-strings zijn byte voor byte identiek, de event-namen zijn beide exact Purchase (hoofdletters doen ertoe), en de id is afgeleid van de bestelling zodat een paginaherlaad hem niet kan veranderen. De velden fbp/fbc/em verbeteren de matchkwaliteit maar zijn niet wat dedupliceert — dat is alleen event_id + event_name.
Hoe ik dit verifieer in echte implementaties
Openheid: ik bouw server-side pipelines voor de kost en ben de oprichter van Fixel Pixel, dus ik heb een voorkeur voor server-side opzetten — precies daarom verifieer ik dedup met aantallen, niet met beweringen.
Mijn protocol bij elk CAPI-traject:
- Inventariseer eerst elke bron. Voordat ik code aanraak, zet ik elk systeem op een rij dat
Purchasekan afvuren: het native Facebook-kanaal, elke CAPI-app, GTM web, server-GTM, thema-pixels. Duplicatie is bijna altijd een extra bron die niemand zich herinnerde, geen kapotte tag. - Draai parallel en lees Test Events, niet het dashboard. Ik vuur een echte purchase af met een
test_event_codeop de server zodat beide tweelingen verschijnen, en bevestig daarna letterlijk het label “Deduplicated” op het gekoppelde paar. - Reconcilieer aantallen tegen bestellingen. Voor een vaste periode vergelijk ik het door Meta gerapporteerde
Purchase-aantal met het werkelijke aantal Shopify-bestellingen. Een gezonde opzet komt dicht bij 1:1 uit; een aantal dat rond 2× de bestellingen ligt, is de vingerafdruk van een gemiste dedup, zelfs als individuele testevents er goed uitzien. - Bevestig dat de id stabiel is. Ik herlaad de bedankpagina en trigger opnieuw om te controleren dat de
event_idniet verandert tussen laadbeurten. Verschuift hij, dan faalt de productie-dedup, ook al slaagde een enkele schone test.
De reconciliatiestap is degene die mensen overslaan, en het is de enige die incidentele duplicatie opvangt.
Veelvoorkomende faalpatronen
- Id’s opnieuw gegenereerd per paginalaad.
event_idopgebouwd uitDate.now(),Math.random(), of een nieuwe UUID bij het renderen. Elke laadbeurt levert een nieuwe waarde op, dus de server-tweeling matcht nooit. Leid hem af van de bestelling. - De afweging order-ID versus willekeurige UUID. Een aan de bestelling ontleende id is stabiel en gratis te reproduceren op de server, maar is raadbaar en herhaalt zich als dezelfde bestelling opnieuw rendert — meestal geen probleem voor dedup. Een willekeurige UUID is niet te raden, maar werkt alleen als je hem één keer genereert en bewaart zodat beide kanten dezelfde waarde uitlezen; twee keer gegenereerd, garandeert hij duplicatie. De meeste Shopify-opzetten zijn veiliger met de aan de bestelling ontleende id.
- Afwijkende hoofdletters en namen.
purchaseversusPurchase, of een custom event aan de ene kant en een standaard event aan de andere. - Verwarring tussen camelCase en snake_case.
eventIDin de Pixel,event_idin de server-payload. Het versturen vanevent_idnaarfbq(ofeventIDin de server-JSON) doet stilletjes niets. - Het 48-uursvenster. Gebundelde of vertraagde server-verzendingen die meer dan 48 uur na het browser-event binnenkomen, dedupliceren niet.
- Verwijderde apps die blijven afvuren. Een verwijderde app waarvan het pixel-snippet of de webhook in het thema blijft hangen.
Beperkingen
Deduplicatie reconcilieert alleen hetzelfde event dat twee keer is beschreven. Het voegt geen werkelijk verschillende events samen, en het redt geen opzet waarin de twee bronnen verschillende dingen beschrijven (bijvoorbeeld: de browser vuurt af op de bedankpagina terwijl de server afvuurt bij het vastleggen van de betaling met een andere id — dat zijn twee events, correct geteld).
Het is bovendien beperkt tot Meta; Meta schoonmaken doet niets voor GA4 of Google Ads, die hun eigen telregels hebben. En een perfect dedup-percentage zegt niets over matchkwaliteit — je kunt foutloos dedupliceren terwijl je events verstuurt die Meta nauwelijks kan toeschrijven. Dedup gaat over één keer tellen; matching is een aparte discipline.
Alternatieven
Als je geen gedeelde event_id kunt doorgeven — bijvoorbeeld een dichtgetimmerde app die hem niet blootgeeft — matcht de fallback van Meta op fbp of external_id plus event_name, maar alleen als het browser-event eerst binnenkomt en beide kanten de identifier consequent dragen. Behandel dat als een vangnet, geen plan.
Het duurzamere alternatief is architecturaal: consolideer naar één server-route zodat er één plek is die de id bezit, en laat de browser-Pixel die vervolgens spiegelen. Dat maakt meestal deel uit van een bredere opzet van server-side tracking of Meta Conversions API, en daarom geef ik over het algemeen de voorkeur aan één goed geïnstrumenteerde pipeline boven drie half gecoördineerde. De mechaniek van die pipeline staat in Server-Side Tracking: voordelen, grenzen en architectuur.
Hulp nodig bij het controleren van Meta Purchase?
Gebruik deze beschrijving om de bronnen en eventidentifiers te laten onderzoeken. De volgende stap is vaststellen wat controleerbaar is en eventuele implementatie afspreken. Je hoeft niet eerst een nieuwe CAPI-app te kiezen.