How to Set Up the Meta Conversions API: A Step-by-Step Guide
Author
Sergej
Date Published
Meta's Conversions API is the server-side counterpart to the browser pixel: your server sends purchase and lead events straight to Meta over HTTPS, so the events survive ad blockers, tracking prevention and script failures. Setting it up is not hard. Setting it up correctly is, because most of the ways it fails are silent — the events arrive, the dashboard looks healthy, and the numbers are wrong. This guide walks through the setup end to end and flags the places where that happens.
In short: Get your dataset ID and a system-user access token, capture the event in your shop backend rather than the browser, normalise and hash the customer data, send it to the Conversions API with an event ID derived from the order number, and make the browser pixel send the same ID so Meta deduplicates. Then verify in Test Events before you trust a single number.
Before You Start
Four things need to exist before any of this works.
- A data source in Events Manager. If you already run the Meta pixel, you have one. CAPI attaches to the same data source rather than replacing it.
- Admin access to the Business portfolio. You cannot create the system user or the token without it, and this is where most setups stall for a week waiting on someone else.
- A server-side hook at order confirmation. Shopify, WooCommerce, Shopware, JTL-Shop and custom stacks all provide one. This is where the purchase event should originate.
- The visitor's consent state, readable on the server. If your consent banner only controls browser tags, stop and fix that first. Everything below assumes the server knows whether marketing consent was given.
Step 1: Find Your Dataset ID
In Events Manager, open Data Sources, select your web data source, and go to Settings. The value labelled Dataset ID is what older documentation calls the Pixel ID — same number, renamed. Meta's own tooling still uses both terms in different places, which causes a lot of unnecessary confusion. If a field asks for a pixel ID and you only have a dataset ID, paste it anyway.
Step 2: Generate an Access Token
There is a shortcut and a proper way. The shortcut is the Generate Access Token link in the Conversions API section of the same Settings page. It works, and it is bound to your personal user account — which means it dies when you leave the company or lose access.
The proper way is a system user. In Business Settings, create a system user, assign your data source as an asset with full control, and generate a token with the ads_management and business_management permissions. The token belongs to the business rather than a person. Copy it immediately — Meta shows it once.
Store it as an environment variable or in a secrets manager. Never in your theme, never in a frontend bundle, never in a tag manager container. A leaked CAPI token lets anyone write events into your ad account and poison your optimisation.
Step 3: Capture the Event in the Backend
This is the decision that determines whether the whole exercise was worth it. If you trigger the CAPI call from a script on the thank-you page, a blocked script means no browser event and no server event — you have added infrastructure and recovered nothing. Trigger it from the order-confirmation hook in your shop instead. The order exists in your database; that is the only source that cannot be blocked.
One consequence to plan for: the backend does not know fbp and fbc unless you put them there. Read the _fbp and _fbc cookies at session start, persist them with the cart or the customer session, and attach them at order time. Skipping this is the single most common reason for a mediocre match quality score.
Step 4: Normalise, Hash, Send
Customer data must be normalised before hashing, or the hash will not match Meta's. Lowercase and trim email addresses. Reduce phone numbers to digits only, including the country code and without a leading plus or zeros. Names lowercase without punctuation. Country as a two-letter ISO code. Then SHA-256, hex-encoded.
What you must not hash: fbp, fbc, client_ip_address and client_user_agent. Hashing those breaks matching entirely, and it is a mistake that survives code review easily because it looks more privacy-conscious, not less.
1import crypto from "node:crypto";23const API_VERSION = "v26.0";45const hash = (value) =>6 crypto.createHash("sha256")7 .update(String(value).trim().toLowerCase())8 .digest("hex");910// "+49 (0)7731 123456" -> "497731123456"11const normalisePhone = (value) =>12 value.replace(/[^0-9]/g, "").replace(/^00/, "").replace(/^0/, "49");1314export async function sendPurchase(order, session) {15 // Consent gate: no marketing consent, no request. Not a filter inside16 // the payload — the call simply does not happen.17 if (!session.consent.marketing) return;1819 const res = await fetch(20 `https://graph.facebook.com/${API_VERSION}/${process.env.META_DATASET_ID}/events`,21 {22 method: "POST",23 headers: { "Content-Type": "application/json" },24 body: JSON.stringify({25 access_token: process.env.META_ACCESS_TOKEN,26 data: [27 {28 event_name: "Purchase",29 event_id: `order-${order.id}`, // must match the pixel's eventID30 event_time: Math.floor(Date.now() / 1000),31 action_source: "website", // required for web events32 event_source_url: order.confirmationUrl,33 user_data: {34 em: [hash(order.customer.email)],35 ph: [hash(normalisePhone(order.customer.phone))],36 fn: [hash(order.customer.firstName)],37 ln: [hash(order.customer.lastName)],38 country: [hash(order.shipping.countryCode)], // "de"39 external_id: [hash(order.customer.id)],40 fbp: session.fbp, // never hashed41 fbc: session.fbc, // never hashed42 client_ip_address: session.ip, // never hashed43 client_user_agent: session.userAgent44 },45 custom_data: {46 currency: order.currency,47 value: order.total,48 order_id: String(order.id),49 contents: order.items.map((i) => ({50 id: i.sku,51 quantity: i.quantity,52 item_price: i.unitPrice53 }))54 }55 }56 ]57 // test_event_code: "TEST12345" <- only while testing, remove for production58 })59 }60 );6162 const result = await res.json();63 if (!res.ok || result.error) {64 // Log and retry. A dropped event here is a conversion Meta never learns about.65 throw new Error(`CAPI failed: ${JSON.stringify(result)}`);66 }67}
Send more identifiers, not fewer. Meta's match quality score rewards each one, with hashed email and phone contributing the most. Every field you can supply legitimately makes the difference between an ad set that finds your buyers and one that guesses.
Step 5: Set Up Deduplication
You are now sending Purchase twice: once from the pixel, once from your server. Without deduplication, Meta counts both, your reported revenue doubles, and the bidding algorithm optimises toward a fiction. Meta matches on two fields, event_name and event_id. If both match on events arriving within 48 hours of each other, only the first is counted. When both arrive within about five minutes, Meta favours the browser event.
Derive the ID from the order number. Meta explicitly recommends this, and it has a property that random UUIDs do not: both sides can compute it independently and arrive at the same string. The pixel takes it as the fourth argument of the fbq call, which is easy to miss.
1// On the order confirmation page — eventID is the 4th argument, not part of the data object2fbq("track", "Purchase", {3 value: 249.90,4 currency: "EUR"5}, {6 eventID: "order-10482" // byte-for-byte identical to the server's event_id7});
The strings must be identical — a difference in casing, a stray space or a prefix on one side only is enough to break the match, and the failure looks exactly like strong performance until someone reconciles the numbers against the shop backend.
Step 6: Test Before You Trust
Open the Test Events tab in Events Manager, copy the test code, and add it as test_event_code in your request. Place a real test order and watch it arrive. Check three things: the event appears once rather than twice, the parameter list shows the identifiers you expect, and no warnings appear about missing or malformed fields. Then remove the test code — events sent with it are not used for optimisation, and leaving it in production is a quiet way to send Meta nothing at all for weeks.
Step 7: Verify in Production
After a few days of live traffic, four numbers tell you whether the setup is real:
- Order coverage. Purchases Meta reports divided by orders in your shop backend. This is the number that matters and the one no vendor dashboard shows you by default.
- Event match quality. Shown per event in Events Manager. Watch the trend after each change rather than chasing an absolute target.
- Deduplication. Events Manager flags duplicate and deduplicated counts. If total purchases look roughly doubled, your IDs are not matching.
- Delivery errors. Your own logs, not Meta's. Failed requests must be retried, and a queue that silently drops them is worse than no CAPI at all.
Six Ways This Breaks Quietly
- Mismatched event IDs. Random values generated separately on each side never match. Derive both from the order number.
- Hashing the wrong fields. Hashed fbp, fbc, IP or user agent are dead weight in the payload.
- Skipping normalisation. "Anna.Berger@Example.com " and "anna.berger@example.com" hash to different values. One matches nobody.
- Losing fbc. If the click ID is not stored at first touch, it is gone by checkout — especially on long consideration cycles, which are usually the expensive orders.
- A test code left in production. Events arrive, appear in Test Events, and are used for nothing.
- An expired or under-permissioned token. Personal tokens die with staff changes. Every request fails, and nobody notices until reporting collapses.
Consent Is Part of the Setup, Not a Footnote
Sending a hashed email address to Meta for audience matching is processing personal data under the GDPR, and the server sending it instead of the browser changes nothing about that. Moving to the Conversions API does not create a consent exemption, and it does not remove your cookie banner.
What it does change is where the decision is enforced. A banner that blocks the pixel but has no say over your backend produces the worst outcome available: a system that respects the opt-out visibly while ignoring it in code. Carry the consent state alongside the order, gate the request on it as in the example above, and log the decision. If you are ever asked to demonstrate that declined users were not forwarded, that log is the answer.
Frequently Asked Questions
Can I remove the Meta pixel once CAPI runs?
No. The pixel is what generates fbp and fbc in the first place, and it captures upstream behaviour your server never sees. Meta expects both and deduplicates between them.
Which events should go server-side?
Start with Purchase. It carries the value, it is the optimisation target, and it is where blocked pixels cost real money. Then add InitiateCheckout, AddToCart and Lead as needed. Sending everything server-side on day one multiplies the surface for mistakes without a matching gain.
Do I need a server-side GTM container?
No. A container is one way to reach the API, not a requirement. A direct call from your backend, as shown above, is simpler and gives you a cleaner event source.
How long until results change?
Reported conversions move within a day. Delivery changes take longer, because the algorithm needs to relearn on the improved signal. Judge the setup on order coverage first and on campaign performance only after a full learning cycle.
Doing This Without Building It
Everything above is one platform. Google Ads, TikTok, Pinterest and Klaviyo each have their own API, their own field names, their own hashing rules and their own version cadence — and each of them changes without asking you. That is the part teams underestimate: not the first integration, but the fifth, and the maintenance of all of them a year later.
Tugus takes that off the table. You embed the script once or install the plugin for your shop system, connect Meta in the dashboard, and events are collected once and delivered to every connected destination in its own format — with event IDs shared across browser and server, identifiers hashed before they leave, and consent enforced per destination. Adding TikTok afterwards is a switch, not a sprint. And because the order data flows through the same pipeline, campaign performance can be measured on contribution margin after returns rather than on gross revenue at checkout.
If you want the wider context first — what server-side tracking fixes, what it costs and where it does not help — start with our guide to server-side tracking for e-commerce.
Skip the token juggling. Connect Meta CAPI with Tugus and see how many of your orders Meta is actually reporting.
Related posts
Server-Side Tracking for E-Commerce: What It Fixes, What It Doesn't, and How to Get It Right
What server-side tracking is, why browser measurement keeps failing, the real pros and cons, and how to run it without breaking GDPR consent.