distinct_id is who PostHog and Mixpanel join on
Short answer
Product analytics is not an ads pixel, but the failure is the same: an event with no person is a row nobody can query. The field name changes; the requirement does not.
Validate a pixel Open the rulepack
PostHog
distinct_id is required on a single capture and on every element of batch. Missing, empty, or absent: not ingested. The endpoint still returns 200. Pixellint flags that as vendor.posthog.body.distinct_id.missing or .empty. Send the same distinct_id you use for that person everywhere else in PostHog, including identify.
Anonymous traffic gets a generated id you later alias. Do not mint a new distinct_id on every event because the helper lived in a request scope. A UUID per POST is a new person per click, which looks like huge volume and zero funnels.
PostHog timestamp is a different trap (ISO 8601, not epoch). Identity and time fail independently. Fix distinct_id first or Insights that group by person are fiction even when the clock is right.
Mixpanel
The track payload is a bare array. The person lives at properties.distinct_id, next to properties.token. An empty distinct_id is a hole. Pixellint flags properties.distinct_id.empty when the key is present and empty. $insert_id is for dedup of retries, not a substitute for the person key. Mixing those two is how a retry looks like a new user with a stable insert id, or a known user with a new insert id that double-counts.
Identify calls and original_id / distinct_id merges are how anonymous becomes known. Firing track with a new distinct_id after login splits the funnel in half. Keep the anonymous id, identify to the user id, then track with the user id. Mixpanel documents that merge. Skipping it is a product analytics incident that ads CAPI will not show.
Segment's names
Segment HTTP calls need userId or anonymousId. There is no distinct_id on the wire. Pixellint flags a call with neither as vendor.segment.body.call_needs_an_identifier. Mixpanel and PostHog destinations map those fields into distinct_id. If you only send traits and no id, downstream distinct_id is empty and the destination looks broken.
Keep anonymousId stable until you know the user, then send both userId and the same anonymousId so the destination can alias. Dropping anonymousId on the first identified track is how you orphan the session. The destination cannot merge what it never received.
Batch rows are people too
A PostHog batch with distinct_id on the envelope and not on each row is a common copy from Segment's batch shape. PostHog wants distinct_id on every event object. Mixpanel wants it on every properties object. A helper that sets the id once on the HTTP client does not fill those paths.
Validate a two-row batch where the second row omits the id. pixellint validate json should complain about that row. A single-event fixture will not catch it.
Check the artifact
Paste the pixel URL or JSON body into the
playground. Same engine as
pixellint validate. Artifacts you test may be stored;
see privacy.