TrackingApril 25, 20264 min read

Facebook CAPI for form submissions: a complete server-side guide

How to send Lead events to Meta via the Conversions API, with eventId deduplication, hashed user data and zero browser tracking. Practical implementation for performance marketers.

capifacebook-ads
Abstract server-side conversion pipeline sending form submissions through a secure backend to ad measurement systems.

If you run Meta Ads to a form, the Conversions API (CAPI) is increasingly important for many paid lead-gen setups. Browser tracking can be unreliable because of cookie restrictions, browser privacy controls, ad blockers and mobile connectivity. The usual fix is to send the same Lead event server-side, deduplicated against the Pixel — which is exactly what CAPI is for.

This is a practical walkthrough of how a CAPI Lead event should look when a form submits, what to hash, what to deduplicate, and where most implementations get it wrong.

Why server-side, even if you have the Pixel

The Pixel fires from the browser, so some events can be missed because of ad blockers, browser privacy controls, cookie restrictions or partial connectivity on mobile. CAPI fires from your backend, so the conversion signal is less dependent on the visitor's browser environment.

When both run at once, Meta needs a way to know they describe the same event — otherwise you double-count. That's the job of event_id: a unique identifier you generate client-side, send to the Pixel as eventID, and forward server-side as event_id. Meta merges the pair on its side. You get coverage without inflation.

What a Lead event needs

A well-formed Lead event sent to CAPI has, at minimum:

  • event_name: "Lead"
  • event_time: a Unix timestamp (within the last 7 days, ideally within seconds of the actual event)
  • event_id: the same string you sent to the Pixel
  • action_source: "website"
  • event_source_url: the URL where the lead converted (for attribution)
  • user_data: hashed PII (em for email, ph for phone, plus fbp, fbc, client_ip_address, client_user_agent)
  • custom_data (optional but useful): currency, value, content_name, lead_qualification

em and ph must be SHA-256 hashed lowercase strings, with the email trimmed and the phone stripped of non-digits and prefixed with the country code. fbp is the cookie value _fbp (browser ID Meta sets). fbc comes from the fbclid query parameter on the landing — when present, you should rebuild it as fb.1.<timestamp>.<fbclid> and persist it for the session.

Deduplication: where most implementations fail

The single biggest mistake is generating two different event_ids for the Pixel and the CAPI call. The duplicate ends up counted twice in attribution, distorting your bid strategy. The rule is simple:

  1. Generate one UUID per form submission, client-side.
  2. Pass it to the Pixel as eventID in the fbq('track', 'Lead', {}, { eventID }) call.
  3. Send it to your server in the same submission payload.
  4. Forward it to CAPI as event_id.

If your form defers the Pixel call (e.g., fires only on a thank-you page), the event_id must still match. Use the form's stable submission ID — never regenerate.

A separate failure mode: the same form gets posted twice (browser refresh, double-submit). Idempotency belongs on your server, not Meta's. Reject duplicates before forwarding to CAPI.

Hashing and consent

Meta requires SHA-256 of normalized values. Normalization is non-trivial in some markets:

  • Email: trim, lowercase, then SHA-256.
  • Phone: strip everything but digits, prepend country code if missing, then SHA-256.
  • Names, city, country: lowercase, no punctuation, SHA-256.

Critically: you should only hash and send what the user consented to share. Under GDPR, sending hashed PII to a third party (Meta) for ad measurement requires consent. The hash doesn't make the data anonymous — Meta has the same hash for the same email across millions of accounts, so it joins the dot. Your CAPI integration must read consent state and fall back to event-only (without user_data) when consent is denied.

A typical request body

{
  "data": [
    {
      "event_name": "Lead",
      "event_time": 1745596800,
      "event_id": "9e1b6b8a-6c0e-4f8d-bce1-c4a3a6f4d2b1",
      "action_source": "website",
      "event_source_url": "https://example.com/funnel/quote",
      "user_data": {
        "em": ["e3b0c44298fc1c149afbf4c8996fb924..."],
        "ph": ["7d793037a0760186574b0282f2f435e7..."],
        "fbp": "fb.1.1745596779000.1234567890",
        "fbc": "fb.1.1745596700000.IwAR0abcd1234",
        "client_ip_address": "203.0.113.42",
        "client_user_agent": "Mozilla/5.0 ..."
      },
      "custom_data": {
        "currency": "EUR",
        "value": 35.00,
        "content_name": "Quote request — solar"
      }
    }
  ]
}

POST it to https://graph.facebook.com/v22.0/<PIXEL_ID>/events?access_token=<SYSTEM_USER_TOKEN>.

Quality signals: aim for EMQ ≥ 6

Meta scores each event with an Event Match Quality (EMQ) score from 0 to 10. Higher EMQ = better attribution and more efficient bidding. Hit ≥ 6 reliably and you'll see Meta's optimizer behave noticeably better. The levers are:

  • Always send em and ph when consented.
  • Always send fbp and fbc when present.
  • Always include client_ip_address and client_user_agent.
  • Send the event within seconds of the actual lead — not in batches an hour later.

A funnel that can send consented em + ph + fbp + fbc + ip + ua usually gives Meta more matching signals than a sparse event. Without fbc, match quality can be weaker — which is why capturing fbclid on the landing matters.

How Fluenx handles this for you

Fluenx fires CAPI server-side on form submissions when the integration and consent state allow it, with event_id generated client-side and forwarded automatically. Hashing follows Meta's normalization, consent state is read from the cookie banner, and fbclid / gclid are captured on the landing without scripts on your side. Configure your Pixel ID and access token once in Settings → Integrations, then use Meta Events Manager to verify deduplication and match quality for your funnel.

Start free, no card →