Analytics API

Behavioural analytics for the websites and applications HLD runs — pageviews and sessions, click and attention heatmaps, scroll reach, form behaviour, Core Web Vitals, campaign attribution and intent inference. One tracker, one read API, and the same numbers HLD sees in its own dashboard.

What this is for

Request logs tell you how many times a page was served. They cannot tell you whether anyone read it. Everything worth knowing about a visit — how far down the page they got, what they clicked and what they clicked that did nothing, whether they started your enquiry form and gave up, which campaign actually brought them — only exists in the browser. This API is the record of that.

Two keys, two jobs

The distinction matters more than anything else in these docs, so it comes first.

NameTypeRequiredDescription
hlda_site_…site key — publicNoShips inside the tracking snippet in your page source. It names where data goes and grants nothing. It is not a secret and does not need protecting.
hlda_live_…read key — secretNoReads your analytics through /v1/analytics/*. Only a SHA-256 hash is stored, so it is shown once at issue time and never again. Treat it like a password.
Warning:The read API sends no CORS headers, deliberately. It cannot be called from a browser, which is what stops a read key from ending up in page source. Call it from your server.

Base URL and authentication

bash
https://hldgroup.org/api/v1/analytics

curl https://hldgroup.org/api/v1/analytics/overview \
  -H "Authorization: Bearer hlda_live_xxxx" \
  -G --data-urlencode "site=hlda_site_xxxx" \
     --data-urlencode "days=30"

A key issued for a single site does not need site at all — it is implied. Call GET /v1/analytics/sites first if you do not know which sites your key covers.

Scopes

Every key carries analytics:read. The rest are additive, so a key can be issued for a reporting dashboard without also handing over an email list.

NameTypeRequiredDescription
analytics:readalways presentNoOverview, timeseries, pages and breakdowns.
analytics:sessionsoptionalNoIndividual visits and their event timelines.
analytics:eventsoptionalNoThe raw event stream, for warehouse syncs.
analytics:heatmapsoptionalNoClick, attention and scroll layers.
analytics:visitorsoptionalNoNames, emails and companies from identify() calls. Without it those fields return null rather than erroring.

Endpoints

NameTypeRequiredDescription
GET /v1/analytics/sitesanalytics:readNoThe sites this key can read. Start here.
GET /v1/analytics/overviewanalytics:readNoHeadline numbers for one site and window.
GET /v1/analytics/timeseriesanalytics:readNoVisits bucketed by hour or day.
GET /v1/analytics/pagesanalytics:readNoPer-page reach, attention, entries and exits.
GET /v1/analytics/breakdownanalytics:readNoAny dimension: channel, campaign, country, device, organisation.
GET /v1/analytics/sessionsanalytics:sessionsNoIndividual visits, and one visit replayed.
GET /v1/analytics/eventsanalytics:eventsNoThe raw event stream, paged forward on time.
GET /v1/analytics/heatmapanalytics:heatmapsNoClick, attention and scroll layers for one page.

Errors

Every failure returns the same envelope, so a client can branch on error.type rather than parsing prose.

json
{
  "error": {
    "type": "forbidden",
    "message": "This key is missing the analytics:heatmaps scope."
  }
}
NameTypeRequiredDescription
unauthorized401NoMissing, unknown, revoked or expired key. The same message for all four, on purpose — the difference would let someone test whether a key ever existed.
forbidden403NoThe key is valid but lacks the scope this endpoint needs.
not_found404NoNo such site, page or session inside this key’s scope. A site belonging to someone else is a 404, not a 403.
invalid_request400NoA missing or unusable parameter. The message names it.
rate_limited429NoPer-key limit exceeded. Retry-After says when.
server_error500NoOur side. Safe to retry.

Rate limits

120 requests per minute per key by default, counted per key rather than per IP — a client calling from a fleet of servers is one caller, and the limit should not depend on how many machines they run it from. Ask HLD if you need it raised; it is a per-key setting.