Skip to main content

April 29, 2026 · 6 min read · Strategy

DINEROUTE BLOG

How we hit Match Quality 9 without collecting a single email

Meta's matching algorithm rewards email and phone, but restaurants almost never have either on the ordering page. Here is how to get to a 7-9 score using only IP, user agent, and the two cookies Meta sets for you.


Meta publishes a chart of Event Match Quality recommendations that lists email and phone as the two most powerful match keys. Hashed email is worth roughly 2-3 points on the 0-10 score on its own. Hashed phone is worth another 1-2. If you can pass both, you are at Match Quality 8 or 9 before the algorithm even looks at IP.

For a restaurant smart-link page, you have neither. The diner clicks an Instagram ad, lands on a page that shows them DoorDash and Uber Eats and ChowNow as options, picks one, and is gone in 9 seconds. They did not log in. They did not give you an email. Asking for one would tank your conversion rate by 60% and add zero ordering value, because the diner is about to type their email into DoorDash anyway.

We ran into this exact problem when we set up Malai Kitchen’s first CAPI integration. The conventional wisdom said the score would cap at 4-5 without PII. We hit 9 the third week, and have stayed there since. Here is what actually moved the score.

What Meta is matching on

Match Quality is Meta’s confidence that a server-side event you sent corresponds to a real user and a real ad click. The matching uses a fingerprint built from whatever signals you provide in the user_data block of the CAPI payload.

The matching keys, in rough order of power:

  1. email (hashed SHA-256) — strongest single signal
  2. phone (hashed) — very strong
  3. external_id (a stable hashed user ID) — strong if cross-session
  4. fbc (the click ID, formatted as fb.{subdomain_idx}.{timestamp}.{fbclid}) — strong if recent
  5. fbp (the browser session cookie) — moderate
  6. client_ip_address + client_user_agent (combined) — moderate
  7. first/last name, city, state, zip (hashed) — weak alone, additive

You do not need all of them. You need enough of them, of high enough fidelity, to give the algorithm a confident match.

The four signals that get you to 9 without PII

Restaurant smart-links can reliably capture four signals: IP, user agent, fbc, and fbp. If all four are present and well-formed on every event, you land in the 7-9 range.

1. Capture the IP server-side, not client-side

The single most common reason restaurant CAPI events come in at Match Quality 3-4 is that the IP is being filled in by JavaScript reading a “what is my IP” service, or it is being captured as 127.0.0.1 because the request came through a proxy or CDN without the right headers.

The fix: capture the IP from the actual HTTP request header on your server. In Astro, it’s Astro.clientAddress. Behind Cloudflare, use cf-connecting-ip. Behind Netlify, use x-nf-client-connection-ip. Behind any reverse proxy, use the leftmost value in x-forwarded-for.

Audit: pick a recent CAPI event in the Events Manager test tool. The IP should look like a real public IPv4 or IPv6 address — 172.58.x.x, not 10.x.x.x (private), not 127.0.0.1 (localhost), not ::1 (IPv6 localhost). If it’s wrong, fix the header capture before doing anything else.

2. Pass the user agent string raw

The user agent should be passed as the exact string from the HTTP request user-agent header. Do not truncate. Do not normalize. Do not strip parentheticals. Meta matches on the full string and any modification can break the match.

Audit: pick an event in the test tool. The client_user_agent should be a long string like Mozilla/5.0 (iPhone; CPU iPhone OS 17_4 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.4 Mobile/15E148 Safari/604.1. If it’s Mozilla/5.0 only, you’re truncating somewhere.

3. Format fbc correctly

The fbc field is not the raw fbclid URL parameter. It is the canonical “click ID” format Meta expects: fb.{subdomain_idx}.{click_timestamp_ms}.{fbclid_value}.

The values:

  • subdomain_idx: 1 for events on the root domain, 2 if your event fires on a subdomain like order.brand.com
  • click_timestamp_ms: the Unix timestamp in milliseconds when the click happened
  • fbclid_value: the raw value of the fbclid URL parameter

Most setups skip the timestamp (“we don’t know when the click happened, so we’ll leave it blank”) and pass fb.1..IwAR3xH7... which Meta rejects as malformed. The fix: when your page first loads with fbclid in the URL, record Date.now() and use that as the timestamp. Pass the well-formed fbc string from then on.

The Meta Pixel does this automatically and stores the result in the _fbc cookie. Read that cookie server-side and pass it through. Do not try to construct fbc yourself if the Pixel has already done it.

4. Make sure fbp is being set

_fbp is the Pixel’s browser session cookie. It’s set by the Pixel base code on first page load with a format like fb.1.{timestamp_ms}.{random_10_digit_number}. It identifies the browser session and is used by Meta to dedup events and tie multi-event journeys together.

If you’re not seeing _fbp in the user_data of your CAPI events, either:

  • The Pixel base code isn’t installed on the page where CAPI fires
  • The Pixel is installed but is being blocked (Brave, uBlock Origin)
  • The cookie is being stripped by a reverse proxy

You can fall back to setting _fbp server-side if you detect the cookie is missing, but the cleaner fix is to make sure the Pixel base code is on every page where you fire CAPI. Even if the Pixel itself is blocked, in some configurations the cookie is still set first.

The optional bonus: external_id

If your smart-link sets a first-party cookie of its own — say, a UUID that identifies the diner’s browser across visits — you can hash it and pass it as external_id. This gives Meta a stable cross-session anchor even though you’ve never collected PII.

The cookie is yours, set on first visit, persisted for 90 days. The hash is SHA-256 of the raw cookie value. Meta does not know what the value represents, but if the same user comes back next week, the same external_id arrives with the new event, and the matching algorithm gets stronger.

This typically lifts Match Quality by 0.5-1 point on top of IP+UA+FBC+FBP.

What does NOT help

  • Geo-IP enrichment (city/state/zip derived from the IP). The IP is the IP. Adding derived fields adds nothing and may add noise.
  • Hardcoded country codes. Meta infers this from the IP already.
  • Hashed but inaccurate emails. If you’re tempted to hash a placeholder like noreply@restaurant.com, don’t. Meta will match it to nothing and the noise can confuse the algorithm.
  • Pixel-only setup with no CAPI. Pixel events are evaluated separately from CAPI events. The Match Quality you see in Events Manager applies primarily to your server-side stream.

How to audit your own setup

  1. Open Events Manager → your Pixel → Diagnostics
  2. Filter by Server (CAPI) events only
  3. Look at the average Match Quality score for the last 7 days
  4. Click any low-scoring event, see which fields are missing or malformed
  5. Compare to the structure above and fix one field at a time

You should see Match Quality move within 48 hours of each fix. The score is calculated rolling-7-day, so it lags a bit but it does respond.

Why this matters

Match Quality is the single best leading indicator of paid-social campaign improvement. We have never seen a restaurant ad account where Match Quality went from 3 to 7 and cost-per-result didn’t drop within the next 30 days. The relationship is not 1:1, but it’s directional and reliable enough that we treat it as the first metric to check on any new restaurant onboarding.

If you’re still optimizing toward Link Click with a Match Quality of 3, the rest of your campaign work is downstream of bad data. Fix the score first. Everything else gets easier.

Related reading:


START FREE

Stop reading. Start measuring.

Every DineRoute restaurant fires Meta CAPI, Google Ads, GA4, and TikTok events server-side from day one. 14-day trial, no card.

No credit card. 14-day trial. Cancel any time.