← All posts
Playbook·12 min read

Meta Conversions API setup guide (2026)

Everything needed to send your first server events to Meta: prerequisites, the anatomy of a CAPI payload, hashing and event_id deduplication rules, testing with test_event_code, the mistakes that quietly ruin match quality, plus an honest look at building it yourself vs. using a tool.


The Meta Conversions API (CAPI) is an HTTP endpoint that lets your server report conversions to Meta directly, instead of relying on the browser pixel alone. You send JSON describing the event (what happened, when, and hashed identifiers for who did it), and Meta matches it to an account, deduplicates it against the pixel, and feeds it to attribution and delivery. This guide walks through a working setup end to end: what you need, what the payload looks like, how to test it, and where implementations usually go wrong.

Prerequisites

Three things, all from Events Manager:

  • A pixel (dataset).CAPI events land in the same dataset as your pixel events. Meta has been folding the “pixel” concept into datasets, but the ID is the same. Grab it from Events Manager → your dataset → Settings.
  • An access token.Under the dataset's Settings, the Conversions API section has a Generate access tokenoption; larger setups create a system user in Business Manager and issue the token there so it isn't tied to an employee's account. Treat it like a password: server-side storage only, never in client code.
  • The browser pixel, kept in place. The recommended architecture is redundant: pixel and server both report, and deduplication keeps one copy. You need the pixel firing with event IDs (below) for that to work.

You will also need somewhere to run the code: your backend, a serverless function, or a managed tool that does the delivery for you (more on that tradeoff at the end).

Anatomy of an event payload

Events are POSTed in batches to /{dataset_id}/events on the Graph API. Each event object has a handful of top-level fields that matter:

FieldWhat it isRules
event_nameStandard or custom event nameCase-sensitive. Purchase, not purchase, and it must match the pixel's spelling exactly for deduplication.
event_timeUnix timestamp (seconds) of the conversionWhen the user converted, not when you sent the request. Events older than seven days are rejected.
event_idYour deduplication keyDeterministic, derived from something both browser and server know, like the order ID. See event_id.
action_sourceWhere the conversion happenedwebsite, app, physical_store, phone_call… The field describes the user's context, not your server.
event_source_urlPage where the event occurredRequired for website events.
user_dataMatching identifiersThe part that decides whether the event is worth anything.
custom_dataValue, currency, contents…value + currency are required for purchase optimization to work properly.

user_data and the hashing rules

Personal identifiers must be normalized, then SHA-256 hashed, before they go in the payload. Hashing is deterministic, so if your normalization differs from Meta's by so much as an uppercase letter, the hashes never match and the field contributes nothing. The short version:

  • Email (em): trim, lowercase, hash.
  • Phone (ph): digits only, with country code, no +, then hash.
  • external_id: your own user or visitor ID, hashed, identical on every event for the same person.
  • Never hash fbc, fbp, client_ip_address or client_user_agent. These arrive raw. fbc carries the fbclid from the ad click and is the field that ties the conversion back to a specific ad.

The full normalization table is in our Event Match Quality guide. Read it before you write a single hash, because mis-normalized fields are the most common silent failure in hand-rolled CAPI code.

event_id: how server and pixel coexist

Both your pixel and your server report the same purchase. Meta keeps exactly one copy, but only if both events carry the same event_name and the same event_id. The ID must be deterministic; independently generated UUIDs can never match. Derive it from the order ID and you get deduplication for free:

// Browser (pixel)
fbq('track', 'Purchase',
  { value: 249.00, currency: 'EUR' },
  { eventID: 'order_58213' }
);

// Server (CAPI) - same conversion, same ID
{ "event_name": "Purchase", "event_id": "order_58213", ... }

action_source: declare it honestly

action_source says where the user converted. A checkout on your site sent server-side is still "website": the field describes the conversion context, not the transport. Marking phone or in-store sales as website (or vice versa) skews attribution logic and is the kind of misdeclaration that gets event sources flagged.

A realistic request

Everything together: a purchase reported server-side, with hashed identifiers from the order record and the click ID captured on the landing page weeks earlier:

POST https://graph.facebook.com/v24.0/{dataset_id}/events
Content-Type: application/json

{
  "data": [{
    "event_name": "Purchase",
    "event_time": 1782432000,
    "event_id": "order_58213",
    "action_source": "website",
    "event_source_url": "https://shop.example/checkout/thank-you",
    "user_data": {
      "em": ["a8f5f167f44f4964e6c998dee827110c..."],  // sha256(email)
      "ph": ["c0e81794384491161f1777c232dc176a..."],  // sha256(phone)
      "external_id": ["9b74c9897bac770ffc029102a2..."],
      "fbc": "fb.1.1781222400000.IwAR2xYzAbCdEf",
      "fbp": "fb.1.1781222400000.1019141",
      "client_ip_address": "203.0.113.7",
      "client_user_agent": "Mozilla/5.0 (iPhone; ...)"
    },
    "custom_data": {
      "currency": "EUR",
      "value": 249.00,
      "order_id": "58213"
    }
  }],
  "access_token": "<TOKEN>"
}

Note the array syntax on em, ph and external_id. The API accepts multiple hashes per field, useful when a customer has two known emails. Single values work too; the arrays are the more general form.

Testing before you trust it

  1. Use test_event_code.Events Manager → your dataset → Test events shows a code (something like TEST4711). Add it as a top-level field next to dataand your events appear in the Test events view within seconds, without polluting production reporting. Remove it when you go live, since test-coded events don't count.
  2. Verify the fields Meta received. The Test events view expands each event: check that hashed fields show as received, fbc/fbp are present, and event_source_url looks right.
  3. Verify deduplication.Fire the pixel and the server event for the same test conversion. Events Manager's overview should show the pair as deduplicated, not as two conversions. If you see doubles, the IDs or event names don't match exactly.
  4. Watch Event Match Quality settle. After a few days of production traffic, check the EMQ score per event. Purchase events built from full order records should score noticeably higher than what the pixel alone managed.

Common mistakes

  • Unhashed PII. Sending a raw email in em is both a matching failure and a compliance problem. Hash everything personal; send fbc, fbp, IP and user agent raw.
  • Missing or random event_id. No ID (or a fresh UUID per environment) means no deduplication. Your reported conversions inflate the moment CAPI goes live, and the fix is mechanical, not philosophical.
  • Mismatched event names. purchase vs. Purchase, CompleteRegistration vs. a custom SignUp. Dedup keys on the name too, and case matters.
  • Wrong action_source. Everything stamped website because that was the example you copied. Declare offline and phone conversions as what they are.
  • Stale event_time.Batch jobs that replay yesterday's orders with now() as the timestamp corrupt attribution windows; jobs that replay week-old orders get rejected outright.
  • Hashes of garbage. sha256(""), sha256("null"), hashes of already-hashed values. Omit fields you don't have: an empty-string hash is a field that exists but never matches, which is worse than absence.

Build it yourself or use a tool?

The API itself is not hard. An afternoon gets a competent developer to a working POST /events. The honest cost sits around it:

  • Click ID capture and persistence. The fbcthat makes events attributable has to be captured first-party on the landing page and survive until conversion, sometimes days or weeks later, past Safari's cookie caps. That is a storage-and-identity problem, not an API problem.
  • Identity stitching. Matching an order to the visitor who clicked the ad, across sessions and devices, is where most of the real engineering lives.
  • Operations. Retries on 5xx responses, token rotation, API version bumps, monitoring that tells you delivery broke before your CPA does.

If you have engineers who will own that plumbing permanently, building is a legitimate choice: you keep full control and pay no vendor. If you don't, this is exactly the category of work a managed layer earns its keep on: RoasProof does the Meta CAPI delivery (capture, identity, hashing, deduplication, retries and per-event delivery monitoring) as a product, at a flat monthly price. Either way, the payload rules above are the same; the only question is who maintains the code that follows them.

And if you want the conceptual grounding first, start with Server-side tracking, explained. It covers why server-side delivery recovers conversions the pixel loses.

Stop reading about signal loss. Start closing yours.

Connect your store, verify your events end-to-end, and watch match quality climb. Free 14-day trial, no credit card required.