pixellint

Identity · engineers

IP and user-agent are sent in the clear

A well-meaning privacy pass over the whole payload hashes the two fields the graph uses as weak, plaintext signals. Match quality drops, and the HTTP 200 does not mention it.

The field names

Meta and Snap: user_data.client_ip_address and user_data.client_user_agent. Pinterest CAPI uses the same pair. TikTok: context.ip and context.user_agent. Reddit CAPI v3: user.ip_address and user.user_agent. IPv4, IPv6, and the browser User-Agent header, copied, not digested.

These are the same values the pixel request already sent as TCP and HTTP metadata. CAPI has to re-attach them because the POST comes from your server, which has a different IP and a different User-Agent. The Graph API sees your Lambda or your sGTM cluster unless you copy the original client values into the JSON.

Adjust S2S uses ip_address. AppsFlyer uses ip. Branch uses user_data.ip. X conversion API uses ip_address and user_agent. Each of those packs has a hashed_plaintext_field rule scoped to those names. Pixellint is not affiliated with those vendors. The field names come from their docs.

What hashing looks like

A 64-character hex string in client_ip_address is the usual bug. It is not an IP. The platform cannot compare it to the address it saw on the click. Pixellint flags that shape on Meta, TikTok, Snap, and Pinterest as hashed_plaintext_field. The pattern is sixty-four hex characters, upper or lower case. A truncated digest will not trip that rule. A real IPv6 address will not either, because IPv6 is not 64 hex with no colons.

The same helper that correctly hashes em will do this if you map every user_data value through SHA-256. Split the list. Identifiers hash. IP, user-agent, fbp, and fbc do not. fbp and fbc look random already. Hashing them is a second, silent miss that hashed_plaintext_field does not cover, because those fields have their own shape check instead.

They are weak, and still required in practice

IP plus user-agent will not save an event with no click id and no email. They still lift match quality when the strong ids are missing, which is most PageView events. Omitting them because they feel creepy is how CAPI-only setups go dark on anonymous traffic.

Send the client values. If you only have the VPC NAT, you are better off omitting IP than claiming your load balancer is the shopper. That failure is a matching article, not a hashing one. A hashed NAT address is two mistakes stacked: the wrong host, and the wrong encoding.

Consent and LDU do not change the encoding. If you are allowed to send IP, send it in the clear. If you are not allowed to send IP, omit the field. Hashing it to 'feel safer' produces a value the vendor cannot use and still puts 64 hex of client material in the log.

Why the server cannot invent them

The CAPI POST User-Agent is axios/1.x, Go-http-client, python-requests, or the sGTM collector. That string is your worker. Meta documents client_user_agent as the browser's user agent rather than your HTTP client's. Copy the header from the page request that caused the event, or from the landing request you stored.

Client hints (Sec-CH-UA) are not a drop-in replacement in these payloads. If you only collected hints, still send the UA string the pixel would have sent. Do not SHA-256 the hints and hope. Do not concatenate hints into a fake UA.

Check the JSON, not Events Manager

Test Events will accept a hashed IP and still show the event. Match quality will sag later. Run pixellint validate json and look for hashed_plaintext_field. The fix is to stop hashing those two keys, not to rotate the access token.

Keep IP and user-agent next to em in the fixture so a future refactor cannot hash the whole object again without failing CI.

// WRONG
user_data.client_ip_address = sha256hex(req.ip);
user_data.client_user_agent = sha256hex(req.get('user-agent'));

// RIGHT
user_data.client_ip_address = req.ip;
user_data.client_user_agent = req.get('user-agent');

Check the artifact

Paste the pixel URL or JSON body into the playground. Same engine as pixellint validate. Nothing leaves the tab.