PostHog Capture Treats an Epoch Timestamp as Ingestion Time

작성자

카테고리:

← 피드로
DEV Community · Aleksander Sekowski · 2026-08-22 개발(SW)

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.

원문에서 계속 ↗