CAPI · engineers · PMs
200 means the server was awake
Vendors optimize the ingest path for throughput. Schema checks are shallow, asynchronous, or missing. PostHog documents that a missing event or distinct_id still returns 200 and is not ingested. Segment's HTTP 200 does not prove track had an event name.
PostHog and Segment document the quiet drop
PostHog capture: missing event or distinct_id still HTTP 200, not ingested. vendor.posthog.body.event.missing and vendor.posthog.body.distinct_id.missing exist because the official docs say the quiet drop is expected. timestamp must be ISO 8601; an epoch is read as ingestion time (vendor.posthog.body.timestamp.invalid).
Segment HTTP Tracking API: a track in a batch needs an event name (vendor.segment.body.track_requires_an_event_name). Every call needs userId or anonymousId (vendor.segment.body.call_needs_an_identifier). timestamp is ISO 8601 (vendor.segment.body.timestamp.invalid). The 200 does not check these. Your retry logic that keys on non-2xx will never see the miss.
GA4 204 is the same lie in another status
Measurement Protocol collect returns HTTP 204. That is 'accepted for processing', not 'your purchase had items'. vendor.google-analytics.body.purchase_requires_ecommerce_fields is currency, value, transaction_id, and items. timestamp_micros that is 13 digits is milliseconds, not microseconds (vendor.google-analytics.body.timestamp_micros.invalid). The 204 will not mention it.
The debug endpoint is where Google will talk back. Production collect will store a misspelled event name as a custom event and move on. Use /debug/mp/collect in CI, then send the same body to prod without debug_mode.
Ads CAPIs return 200 with errors in the body
Meta often returns 200 with an error object in the JSON, or accepts the batch and reports per-event errors later. Read the JSON. A gateway 200 on your proxy in front of CAPI is even less information: it means nginx was up.
access_token missing is vendor.meta-conversions-api.param.access_token.missing if you lint the request. If you only log status codes, a 200 with messages[].error is a silent miss. TikTok and Snap behave like other high-throughput ingest: do not assume a 2xx mapped every field.
What to assert instead
Required fields, types, timestamp units, hashed versus raw, ISO currency. That is what Pixellint rulepacks encode from vendor docs. Run them in CI on fixtures, and on a sampled live payload if you can do it without shipping PII to a laptop.
Assert event_name, event_time digits, action_source enum, user_data present, event_id present on hybrid setups, Purchase value and currency. Assert PostHog event plus distinct_id. Assert Segment track event plus userId or anonymousId. Status code is not on that list.
Dashboards lag, and they graph the wrong name
Even a well-formed event can take minutes to hours to show. Absence in the UI is not a 404. Presence in the UI is not proof the payload matched the contract you intended. Purchse still graphs. It graphs as a custom event with no value schema.
CI on the JSON is the fast signal. Events Manager is the slow one. Using the slow one as your only test is how a timestamp-unit bug lives for a week while ROAS looks 'a bit off'.
Lint the body, ignore the smile
A capture without distinct_id still 200s. A Purchase without currency still 204s. The contract is the fields.
pixellint validate json '{"event":"order_completed"}' --rulepack vendor/posthog
# error vendor.posthog.body.distinct_id.missing
# (HTTP 200 from /capture does not report this)
pixellint validate json '{"type":"track","userId":"u1"}' --rulepack vendor/segment
# error vendor.segment.body.track_requires_an_event_name
pixellint validate json @ga4.json --rulepack vendor/google-analytics
# error vendor.google-analytics.body.timestamp_micros.invalid
# when timestamp_micros is 13 digits. Collect still returns 204.
Check the artifact
Paste the pixel URL or JSON body into the
playground. Same engine as
pixellint validate. Nothing leaves the tab.