Methodology

Meta Conversions API Envelope Reference — canonical-to-Meta transformation, enhanced-conversion hashing, test events

Reference doc for the Meta Conversions API side of the server-side conversion attribution stack. The canonical-to-Meta transformation, the user-data hashing requirements, the Test Events tool, and the deduplication mechanics that hold the dedupe rate above 95 percent.

Meta's Conversions API is a single POST and a hash function — which is exactly why teams ship it broken and never notice. The envelope is simple; the dedupe key, the hashing rules, and the consent gate are where the signal quietly leaks.

This is the Meta half of the server-side conversion attribution stack: the request envelope, the canonical-to-Meta transformation, the PII hashing rules that differ from Google's, the Test Events verification surface, and the five failure modes that drag your dedupe rate below the 95% the pillar demands. It assumes the prerequisites are in place — canonical event schema, Worker fanout, consent propagation — and that you have a Meta Pixel ID provisioned in Events Manager.

The request envelope

One POST to https://graph.facebook.com/v19.0/{pixel_id}/events with a JSON body:

{
  "data": [
    {
      "event_name": "Lead",
      "event_time": 1716732738,
      "action_source": "website",
      "event_id": "crm-record-abc-123:lead-milestone",
      "event_source_url": "https://nobrainermedia.com/contact/",
      "user_data": {
        "em": ["5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8"],
        "ph": ["5994471abb01112afcc18159f6cc74b4f511b99806da59b3caf5a9c173cacfc5"],
        "fn": ["c1c224b03cd9bc7b6a86d77f5dace40191766c485cd55dc48caf9ac873335d6f"],
        "client_ip_address": "203.0.113.42",
        "client_user_agent": "Mozilla/5.0 ...",
        "fbp": "fb.1.1716732738123.456789012",
        "fbc": "fb.1.1716732738123.IwAR0..."
      },
      "custom_data": { "value": 7500.00, "currency": "USD", "order_id": "crm-record-abc-123" }
    }
  ],
  "test_event_code": "TEST12345"
}

Eight fields warrant operator attention:

  • event_name maps your CRM milestone to Meta's vocabulary (Lead, Schedule, Subscribe, Purchase) or a custom name. Keep the mapping stable — an event that fires as Lead for three months and lead_milestone the fourth confuses the bidder.
  • event_time is Unix seconds (not milliseconds, not ISO 8601). Events older than 7 days are rejected by default.
  • action_source is website for a web-traceable lead, system_generated for a purely CRM-internal milestone. The wrong value degrades the optimization signal.
  • event_id is the dedupe primitive. With the fbp/fbc cookies it lets Meta reconcile the server event against any legacy client-side pixel. Stable, unique per conversion, matched across both fires — this is the field that delivers the pillar's 95%+ dedupe rate.
  • user_data carries PII hashed per Meta's rules (below). Field codes (em, ph, fn) are Meta-specific; the Worker renames email → em, phone → ph at the boundary.
  • custom_data.value / currency denominate closed revenue in ISO 4217 — multi-currency is involved enough that a dedicated spoke covers it.
  • custom_data.order_id is the operator-side dedupe key (distinct from event_id), used to reconcile against a payment processor's own CAPI integration firing for the same order.
  • test_event_code routes events to the Test Events tab instead of production attribution. Omit it in production.

PII hashing — where Meta differs from Google

Hash every PII field as lowercase, trimmed, hex-encoded SHA-256, each wrapped in an array (Meta requires ["…"], not "…"). Three places differ from Google Ads and bite teams that reuse one hash function for both:

  • Phone. Digits only, no country-code prefix. (415) 555-0100 becomes 4155550100 — Google prefixes with + and country code, Meta does not. A transform that serves both platforms must hash phone twice.
  • State and country. Two-letter codes, lowercased: ca not california, us not the full name. No abbreviation expansion.
  • Names. Lowercase and trim, but preserve hyphens and apostrophes — do not strip them.

The client_ip_address and client_user_agent stay unhashed; they lift match rates for users without fbp/fbc cookies and should always be populated when available.

Want CAPI wired with a dedupe rate that holds above 95%? Talk to the team that runs it. →

The canonical-to-Meta transformation

The transform is a pure function — per-field rename plus per-field hash, no I/O, no side effects — living in the Worker's /src/transforms/meta.ts. Pure functions are unit-testable, and the Meta envelope is among the most-tested modules in a production fanout layer:

function canonicalToMeta(
  canonical: CanonicalEvent,
  pixelId: string,
  consent: ConsentState
): MetaCAPIEvent {
  // returns the data[] element of the request body
}

Three transformations earn naming. event_name mapping is a static lookup table — new milestones get an explicit entry, never a string-similarity guess. Dedupe-key generation composes event_id as {record_id}:{milestone_name}, so a record moving lead → booked-call → signed-contract yields three independently-deduped IDs. Consent-to-LDU translation lives here, not in the ingestion layer, because consent rules are platform-specific.

The Test Events tool

Events Manager → Pixel → Test Events is the single highest-leverage verification surface in the whole stack — operators who skip it ship wire-ups that pass code review and silently fail in production. Generate a test code, attach it to test_event_code, fire from the Worker, and watch events land within ~5 seconds. Three gates fire there:

  • Envelope validity — required fields present, types correct, event_time in window. Failures show as partial_failure entries and red banners.
  • User-data match quality — a per-field status (green/yellow/red) plus a match-quality score. A low score means the hashing canonicalization is firing wrong.
  • Dedupe registration — a successful dedupe shows both the server and client events sharing one event_id and a single counted conversion.

The five failure modes

  • event_id mismatch between server and client. Whitespace, casing, or format drift makes Meta count both. Generate the ID at ingestion and push it to the legacy pixel's eventID via the data layer.
  • Wrong action_source. system_generated doesn't enrich the bidder like website does. Source it from the CRM milestone, not a Worker default.
  • Hashing drift. Uppercase emails, phone-with-prefix, abbreviated states. Unit-test the canonicalizer against Meta's published examples.
  • Missing fbp/fbc. Without them, matching falls back to user-data alone and match rate drops materially. Capture both at landing-page load and persist them through the CRM record.
  • LDU mis-application. Fire it only for California traffic with withheld consent — gating on explicit consent plus geolocation, never as a default.

Closing

Meta CAPI is the most operationally subtle of the five platform wire-ups. The envelope is simple, the canonicalization is tractable, the failure modes are all explicit — what makes it subtle is propagating the event_id correctly between the server fire and any legacy pixel. Run the Test Events tool during week 3 of the build and you catch every failure category before cutover. Skip it and you find them three weeks later, when the CPA curve has drifted and the bidder has un-learned what it knew.

Ready to ship Meta CAPI correctly the first time? Book a 30-minute call →

Share X LinkedIn
Build it yourself?

Get the kit, not just the theory.

We'll send the build checklist behind this post — and the next pillar when it ships. One email, no drip sequence. Unsubscribe in one click.

Want this built for you?

Book a discovery call. We'll walk your numbers.

20 minutes. Tell us what's broken, hear what we'd ship in the next 90 days. No pitch deck.