Analytics · engineers · PMs
PostHog returns 200 and drops the event
This is the poster child for 200-means-awake. Pixellint's PostHog pack exists because official docs say the quiet drop is expected ingest behavior.
Required on every capture
api_key, event, and distinct_id are required. api_key is the project key. Without it PostHog rejects the request. Without event or distinct_id, official docs say the call still returns 200 and the event is not ingested. That is not a Pixellint invention. Pixellint flags vendor.posthog.body.event.missing and vendor.posthog.body.distinct_id.missing (empty distinct_id too) so CI fails where the HTTP layer will not.
The same three fields apply on every row of a batch. A batch of 50 with one blank distinct_id is 49 insights rows and a hole you will not see in the 200. Alias and identify still need the same distinct_id the captures used. Rotating it per request splits the person.
{
"api_key": "phc_example",
"event": "purchase",
"distinct_id": "user-123",
"timestamp": "2026-08-21T18:04:00Z",
"properties": { "currency": "USD", "value": 19.99 }
}
ISO 8601 is event time. Epoch is ingestion time.
PostHog documents timestamp as ISO 8601, for example 2026-08-21T18:04:00Z. A numeric epoch in timestamp is a different type. Capture still accepts the request. The historical value is not applied. The event files under received-at. Backfills look like a spike today. Pixellint: vendor.posthog.body.timestamp.invalid.
Do not reuse the Amplitude column. Amplitude wants 13-digit milliseconds. Segment wants the same ISO shape PostHog wants. Convert at the edge. If you omit timestamp, now is correct for live traffic and wrong for a worker that retries at drain time.
pixellint validate json @capture.json --rulepack vendor/posthog
Single capture versus batch
POST /e/, /capture/, /batch/, or /i/v0/e on a posthog.com host. A single event sits at the root. A batch repeats event and distinct_id on every element. Properties.distinct_id is not a substitute for the top-level field the pack reads. Keep the project key server-side. A phc_ key in a public repo is a write handle to your project.
Self-hosted PostHog uses the same JSON. The pack matches host_suffix posthog.com and the JSON paths. A bare body with no URL is distinguished from Segment by the keys only PostHog uses: api_key and distinct_id. writeKey, userId, and anonymousId rule this pack out.
Properties are not a second identity
email in properties is useful for people search and a PII log. Capture is a product analytics pipe, not an ads CAPI. Do not SHA-256 the email the way you do for Meta and then wonder why PostHog cannot find the person. Do not put passwords, session tokens, or full payment payloads in properties because a debug helper dumped the request.
Group analytics, person properties, and event properties are three slots. Setting plan=pro on the person and never sending an event named subscribed will confuse PMs who live in Insights. The validator checks the capture contract, not your taxonomy.
200 is not a contract test
A warmup probe that asserts status 200 on /capture/ will pass on a body with no event name. That is the incident. Pair the probe with pixellint validate json. Official docs are the reason the pack exists: missing event or distinct_id still 200, not ingested. Treat that sentence as the test plan, not as folklore.
Pixellint is not affiliated with PostHog. The pack cites the capture API. If the docs move, the pack should move. Your HTTP client retrying 200s will not notice a drop.
Check the artifact
Paste the pixel URL or JSON body into the
playground. Same engine as
pixellint validate. Nothing leaves the tab.