Identity · engineers
Send the client IP, not the VPC NAT
Your POST comes from us-east-1; the shopper came from a coffee shop. If you send the server address as client_ip_address, Meta tries to match a data center, which is worse than sending nothing.
Where to read the IP
From the original browser request: X-Forwarded-For (left-most public hop you trust), or the socket address if nothing sits in front. Do not use the IP of the CAPI worker, the sGTM cluster, or the Lambda that forwarded the event. CloudFront, ALB, and nginx each append. If you take the right-most hop, you often get yourself.
IPv6 should stay IPv6. Do not compress it into IPv4 mapped form unless that is what the browser actually used. Dual-stack mismatches look like two people. A hashed IPv6 is still hashed_plaintext_field, not a clever compact form.
Privacy-sensitive regions may give you a truncated IP from the CMP or from the edge. A truncated IP is still plaintext. A SHA-256 of the full IP is not an improvement. Omit rather than invent. Pixellint will not geolocate for you and will not check that the address is public unicast. It will check that you did not put 64 hex in the IP slot.
User-Agent
Forward the browser User-Agent header from the page request. Axios, Go http.Client, and Python requests all send their own UA on the CAPI POST. That string is not the shopper. Meta documents client_user_agent as the browser's user agent rather than your HTTP client's.
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. Reduced User-Agent in Chromium is still a UA string. Send that string. Do not reconstruct a 2019 UA from hints to 'help matching'. The graph has the same reduced UA the pixel would have sent.
sGTM and reverse proxies
Server-side GTM sees the browser hit to your first-party collector, which is the right IP, then fans out. A custom CAPI helper that starts from an order webhook has no IP unless you stored it at landing or the page posted it in. Store it on the session next to the click ids.
Omit client_ip_address rather than inventing one. A hashed IP is also wrong. Weak identifiers only help when they are true. Cloudflare or Fastly as the collector can pass the visitor IP in a header you configure. Read that header in sGTM. Do not assume event.data.ip is already the shopper without checking the template.
NAT, VPN, and coffee shops
Carrier-grade NAT and VPN egress mean many users share an IP. That is why IP plus UA is weak. It is still better than the IP of your ECS task, which is shared by every conversion you send. Corporate offices look like one person if you only send IP. Pair with email, fbp, or a click id.
Sending 127.0.0.1, 10.0.0.0/8, or the Kubernetes pod IP is a matching own-goal. If your parser failed and defaulted to localhost, omit the field. Pixellint does not currently forbid RFC1918 addresses. That absence is not permission. It is a gap. Do not treat a clean validate as proof the IP is the shopper.
Hashing is a different bug
If client_ip_address is 64 hex, fix hashing first (hashed_plaintext_field). If it is 3.218.x.x and you run in us-east-1, you are sending AWS. If it is the shopper and match quality is still low, you are missing strong ids, not UA formatting.
Pixellint is not affiliated with Meta or TikTok. Run pixellint validate json, then read the payload with a human. The pack cannot know whether 203.0.113.10 is the buyer or the NAT.
// WRONG: worker identity
user_data.client_ip_address = outboundNat;
user_data.client_user_agent = 'axios/1.6.0';
// WRONG: hashed
user_data.client_ip_address = sha256hex(shopperIp);
// RIGHT: original request
user_data.client_ip_address = shopperIp;
user_data.client_user_agent = shopperUa;
Check the artifact
Paste the pixel URL or JSON body into the
playground. Same engine as
pixellint validate. Nothing leaves the tab.