You backfill six months of user signed up events into PostHog through the capture API. The request returns 200. Insights show a spike today, not a curve across 2025.
The events are not missing. They are stamped with the time PostHog received them.
{
"api_key": "phc_abc123",
"event": "user signed up",
"distinct_id": "user-10024",
"timestamp": 1770000000
}
Enter fullscreen mode Exit fullscreen mode
PostHog documents timestamp as ISO 8601. An epoch number is a different type. Capture still accepts the request. The historical value is not applied. The event lands at ingestion time.
"timestamp": "2026-07-26T06:00:00Z"
Enter fullscreen mode Exit fullscreen mode
I maintain Pixellint, an open source linter for pixels and analytics payloads. The PostHog pack (vendor/posthog) contracts single capture and batch against that capture API page. This is the finding on an epoch timestamp:
$ pixellint validate json @capture.json
rulepack: vendor/posthog (vendor: posthog)
error vendor.posthog.body.timestamp.invalid
`timestamp` is `1770000000`, which does not match
`^[0-9]{4}-[0-9]{2}-[0-9]{2}[T ][0-9]{2}:[0-9]{2}:[0-9]{2}`.
PostHog documents the timestamp as ISO 8601. An epoch number is
read as the ingestion time instead.
fix: Send an ISO 8601 timestamp, such as `2026-07-26T06:00:00Z`.
docs: https://posthog.com/docs/api/capture
Enter fullscreen mode Exit fullscreen mode
Segment’s HTTP API uses the same ISO 8601 rule. Amplitude wants the opposite: milliseconds since epoch, 13 digits, and a 10-digit seconds value lands in 1970. If your pipeline already emits epoch for Amplitude, do not reuse the field on PostHog.
Every event needs a distinct_id
distinct_id is required, on a single capture and on every element of batch. PostHog’s capture docs are explicit about the failure mode: a missing event name, a missing distinct_id, or an empty distinct_id is not ingested, and the endpoint still returns 200 OK. The status code is not the contract.
{
"api_key": "phc_abc123",
"event": "user signed up",
"properties": { "plan": "pro" },
"timestamp": "2026-07-26T06:00:00Z"
}
Enter fullscreen mode Exit fullscreen mode
error vendor.posthog.body.distinct_id.missing
`distinct_id` is required on PostHog capture requests but is not
present. It identifies the person the event belongs to.
fix: Send the same `distinct_id` you use elsewhere for this person.
Enter fullscreen mode Exit fullscreen mode
event is required the same way. A nameless capture is not an event.
api_key is required on the envelope. The project key from project settings, not a personal API key. Capture rejects the request without it; the pack fails the payload before you find that out from a 401.
Batch is the same contract, per row
A batch body is an array of the same event object. The pack walks every row. One missing distinct_id in a 500-event backfill is one error, not a silent hole in the middle of the file.
{
"api_key": "phc_abc123",
"batch": [
{ "event": "user signed up", "distinct_id": "user-1", "timestamp": "2026-01-01T00:00:00Z" },
{ "event": "user signed up", "timestamp": "2026-01-02T00:00:00Z" }
]
}
Enter fullscreen mode Exit fullscreen mode
The second row is the one that fails. The first is fine. That is the point of checking the body before the POST.
A real historical backfill also needs historical_migration: true on the batch so PostHog does not treat the volume as a spike. That is an ingest flag, not a shape check, and the pack does not invent a rule for it.
What the CLI actually prints
The pack matches posthog.com capture paths (/e/, /capture/, /batch/, /i/v0/e) on a URL, and it matches a JSON body that carries distinct_id or event (and does not look like Segment or Amplitude). You can lint the body in a fixture with no host at all.
pixellint validate json @capture.json
Enter fullscreen mode Exit fullscreen mode
rulepack: core
ok
rulepack: vendor/posthog (vendor: posthog)
error vendor.posthog.body.timestamp.invalid ...
1 error(s), 0 warning(s), 0 info message(s) across 2 rulepack(s).
Enter fullscreen mode Exit fullscreen mode
Exit code 1. Same catalog from Node:
import { validate, isOk } from "pixellint";
const summary = validate(JSON.stringify(payload), { kind: "json" });
isOk(summary); // false
Enter fullscreen mode Exit fullscreen mode
cargo install pixellint
# or: npm install pixellint
Enter fullscreen mode Exit fullscreen mode
The contracts, with citations, live on the PostHog capture pack page. Paste a body into the browser playground if you do not want a clone. Nothing you paste leaves the browser.
The short version
timestamp is ISO 8601, not an epoch number. An epoch value does not backfill; it stamps now. Every event needs event, distinct_id, and the project api_key. Batch rows are checked one at a time.
Pixellint is independent of PostHog. The rule ids above cite the capture API docs because that is where the requirements live, not because this is an official tool.