Skip to main content

Analytics

When they come

Every hour of every weekday, as a grid.

GET/sites/{siteId}/heatmap
When they come
curl --request GET \
  --url 'https://truestat.io/api/v1/sites/{siteId}/heatmap' \
  --header 'Authorization: Bearer <token>'
{
  "data": [
    {
      "day": 3,
      "hour": 14,
      "views": 144,
      "visitors": 45
    }
  ],
  "meta": {
    "range": {
      "to": "2026-09-16T00:00:00.000Z",
      "tz": "Europe/Istanbul",
      "from": "2026-09-09T00:00:00.000Z",
      "clamped": false
    },
    "site_id": "6fed8980-a119-4a9c-a4d4-53e448ef9f9e"
  }
}

Your traffic folded onto a single week: 7 days × 24 hours.

This answers "when", which a line chart cannot

On a thirty-day line every day collapses to one point, so the difference between Tuesday mornings and Friday evenings is invisible. This sums the same traffic into weekday-and-hour cells, which is where that pattern shows up.

The rows are not a time series. day is a weekday, not a date, and one cell holds every Tuesday 09:00 in the window added together. Asking for a longer window makes each cell larger, not the grid wider.

day is 1 = Monday

Not 0 = Sunday, which several date libraries use.

The numbering is published exactly as the query produces it rather than shifted to match any particular library. A silent renumbering is what puts Sunday's traffic on Monday's row in somebody's chart, and it is invisible once it has happened.

day
1Monday
2Tuesday
3Wednesday
4Thursday
5Friday
6Saturday
7Sunday

hour is in your timezone

Unlike the other bucketed endpoints, this one takes tz into the query itself. It has to: "when do people visit" asked in UTC about European readers answers a question nobody has.

Pass tz and the grid is the reader's own clock:

GET /sites/{siteId}/heatmap?period=last30d&tz=Europe/Istanbul

Verified: a burst of 432 views sitting at hour 10 in UTC appears at hour 13 with tz=Europe/Istanbul. The parameter moves the grid, it does not just label it.

The grid is sparse

An hour with no traffic has no row. At most 168 cells come back, usually far fewer.

Fill the missing ones with zero when you draw it. An absent cell is an empty hour, not a gap in the data — and a chart that skips them silently compresses the quiet parts of the week.

Its own channel filter

channel narrows the grid to one source, separate from the shared filter_* set so the grid can refresh on its own while the rest of a dashboard stays put:

GET /sites/{siteId}/heatmap?period=last30d&channel=Organic%20Search

Useful for the question the grid is best at: whether your paid traffic arrives at a different time of day than your organic traffic.

Authorizations

Authorizationstringheaderrequired

An API key from Settings → API keys. Scopes available: analytics:read, sites:read, sites:write, settings:read, settings:write, goals:read, goals:write, funnels:read, funnels:write, notes:read, notes:write, alerts:read, alerts:write, team:read, team:write, revenue:read, revenue:write.

Bearer authentication header of the form Bearer <token>, where <token> is your auth token.

Path parameters

siteIdstring (uuid)pathrequired
The site's id, from `GET /sites`. A site that is not yours and a site that does not exist both answer 403, so this cannot be used to discover other people's ids.

Query parameters

channelstringquery
Narrow to one traffic source — `Organic Search`, `Direct`, `AI`. This endpoint's own filter, separate from the shared `filter_*` set, so the grid can refresh on its own.
periodenum<string>query
A named window, resolved in `tz`. Cannot be combined with `from`/`to` — a request carrying both is refused rather than guessed at.
options:todayyesterdaylast24hlast7dlast30dlast12mmonthyearall
fromstring (date-time)query
Start of the window, ISO 8601 with an offset. Must be given together with `to`.
tostring (date-time)query
End of the window. Must be given together with `from`.
tzstringquerydefault: UTC
IANA timezone for day boundaries. `period=today&tz=Europe/Istanbul` means the reader's today, not the server's. Defaults to UTC.
botsenum<string>querydefault: excluded
Whether crawler traffic is counted. Excluded by default — the `/ai/*` endpoints report it separately and always include it.
options:excludedincluded

Response

application/json

The answer.

dataunknown
See the example.
metaobject