How to query active users from the PostHog API
Part of PostHog active users on your Mac
Which PostHog endpoint answers "how many active users"?
The query endpoint, with HogQL. PostHog has an insights API and a trends API, but both make you construct a filter object and then interpret an output shape designed for a chart component.
HogQL is SQL. You write what you want and get back columns you named.
curl -s 'https://eu.posthog.com/api/projects/12345/query/' \
-H 'Authorization: Bearer phx_...' \
-H 'Content-Type: application/json' \
-d '{
"query": {
"kind": "HogQLQuery",
"query": "SELECT uniq(person_id) AS value FROM events WHERE timestamp >= toDateTime(1759276800) AND timestamp < toDateTime(1759881600)"
}
}'
The response carries a results array of rows, matching the columns you
selected. No shape to reverse-engineer.
uniq, and why the number is smaller than you expected
uniq(person_id) de-duplicates across everything the query covers. Over a
week, somebody active on three days counts once.
That means a weekly active user count is not the sum of seven daily counts, and it is not the average either. It is strictly smaller than the sum and usually much smaller.
If you want a daily series alongside the de-duplicated total, that is two queries, not one:
-- The series. Each day de-duplicated within itself.
SELECT toUnixTimestamp(toStartOfDay(timestamp)) AS bucket, uniq(person_id) AS value
FROM events
WHERE timestamp >= toDateTime({from}) AND timestamp < toDateTime({to})
GROUP BY bucket ORDER BY bucket
-- The headline. De-duplicated across the whole window.
SELECT uniq(person_id) AS value
FROM events
WHERE timestamp >= toDateTime({from}) AND timestamp < toDateTime({to})
Running only the first and summing it is the single most common PostHog reporting bug, and it inflates the figure by however much your users come back, which is exactly the thing you were trying to measure.
person_id, not distinct_id
distinct_id is per device and per session identifier. person_id is
PostHog's resolved person after identity merging, so the same human on a
phone and a laptop is one person.
Counting distinct_id counts devices. Sometimes that is what you want. It is
almost never what somebody means by "active users".
toStartOfWeek starts on Monday
ClickHouse's toStartOfWeek defaults to Sunday. PostHog's own dashboards
default to Monday. The two disagree by a day and the difference is invisible
until somebody compares a weekly chart against the UI.
toStartOfWeek(timestamp, 1) -- 1 = Monday
Timezone
HogQL evaluates in the project's timezone, which is what you want: it makes
your query agree with the PostHog UI. If you need a different one,
toTimeZone(timestamp, 'Europe/London') is explicit, and being explicit is
worth the extra characters in anything that will be read later.
Name your queries
{ "query": { "kind": "HogQLQuery", "query": "..." }, "name": "notch:active_users" }
PostHog surfaces query names in its own usage views. Anybody looking at who
is hammering their project sees a name rather than an anonymous SELECT,
which is the difference between a question and an incident.
Or read it off the notch
Notch Metrics queries PostHog this way and reports active users in the MacBook notch, with the headline de-duplicated across the window and each bar de-duplicated within its own day. The page says so rather than leaving you to discover it.
New users, events, pageviews and sessions sit beside it. Everything PostHog reports, or download Notch Metrics.
- posthog
- analytics
- api