Docs
Next

Analytics reference

Configuration, endpoints and caching.

On this page

The full analytics option, the endpoints the widgets call, and how responses are cached.

Configuration referenceLink to this section

analytics?: false | {
  provider:                    // discriminated union on `source`
    | {
        source: 'ga4'
        propertyId: string     // the numeric GA4 property id
        clientEmail: string    // service account client_email from its JSON key file
        privateKey: string     // service account private_key; literal \n sequences are normalized
      }
    | {
        source: 'plausible'
        apiKey: string         // Stats API key (sent as a Bearer token)
        siteId: string         // the site's domain exactly as configured in Plausible
        host?: string          // self-hosted instance base URL; default: plausible.io
      }
    | {
        source: 'umami'
        websiteId: string      // the website id (UUID) from the Umami dashboard
        apiKey?: string        // Umami Cloud API key — when set, username/password are not needed
        host?: string          // self-hosted instance base URL; required for username/password auth
        username?: string      // self-hosted login (paired with password)
        password?: string
      }
    | {
        source: 'matomo'
        host: string           // Matomo instance base URL
        siteId: number | string // the numeric site id (idSite)
        tokenAuth: string      // API token — only ever sent in POST bodies, never in URLs
      }
    | {
        source: 'fathom'
        apiKey: string         // Fathom API key (sent as a Bearer token)
        siteId: string         // the Fathom site id (e.g. 'ABCDEFGH')
      }
    | {
        source: 'custom'
        adapter: CustomAnalyticsAdapter // bring your own backend — see "Custom provider" below
      }
  cache?: false | {            // false disables kv persistence (request dedup stays active)
    ttlSeconds?: number        // TTL for timeseries/aggregate/breakdown responses; default 300
    liveTtlSeconds?: number    // TTL for the live-visitors response; default 30
  }
}

All credentials are server-only: they live in the server-side config store and never reach the admin client — the settings endpoint exposes the provider name (plus a custom adapter's display label) and never exposes a host, id, or key on its own. A dashboardUrl deep-link is the one exception: when the provider can build one, it may embed a destination identifier (e.g. a Plausible site domain, a GA4 property id) baked into the URL the widgets link out to.

EndpointsLink to this section

Registered on the Payload config only when analytics is enabled. The data endpoints require authentication + the analytics.read capability (401 unauthenticated → 404 disabled → 403 missing capability → 400 invalid query params → 502 provider failure); /settings requires only authentication — it carries no secrets and returns a canRead flag the widgets use:

EndpointMethodPurpose
/api/systhema/analytics/timeseriesGETZero-filled pageviews + visitors series — ?preset=28d or ?from=&to=, plus &tz= (the viewer's IANA timezone) and optionally &compare=true for a previous-period overlay. Hourly buckets for a single-day span when the provider supports it.
/api/systhema/analytics/aggregateGETAll five metrics (pageviews, visitors, sessions, bounceRate, avgDuration) for the current period plus the previous period for deltas — same range params (no compare; the previous period is always included).
/api/systhema/analytics/breakdownGETTop-N rows for one property — ?property=&limit= (limit 1–50, default 10) plus range params. property values: page, source, country, region, city, device, browser, os, language, age, gender. Requesting a property the configured provider can't serve (see the capability matrix below) returns 400.
/api/systhema/analytics/liveGETCurrent live-visitor count (no range params — it always reflects right now).
/api/systhema/analytics/settingsGETSafe client settings — enabled/canRead, the provider name, the provider-filtered presets and breakdownProperties vocabularies, the provider's realtime window, and a dashboardUrl deep-link to the provider's own dashboard (null when not resolvable). Never exposes keys, hosts or ids on their own — only baked into dashboardUrl where the link itself needs them (e.g. a Plausible domain).

Successful data responses are { success: true, data, stale? } — stale: true rides along only when an expired cache entry was served because the provider refresh failed (see Caching). Provider error details are logged server-side only; the client sees a generic 502 message, since provider errors can carry hosts, URLs or ids.

CachingLink to this section

Provider responses are cached server-side in payload.kv (Payload's native database-backed key-value store) under systhema-analytics:* keys. Defaults: 300 seconds for timeseries/aggregate/breakdown, 30 seconds for the live count — tune via cache.ttlSeconds / cache.liveTtlSeconds.

  • Serve-stale-on-error: when a cached entry has expired and the provider refresh fails, the stale data is returned (flagged stale: true in the response and shown as a "stale" hint in the widgets) instead of an error. A failed fetch is never persisted.
  • Request deduplication: concurrent requests for the same data share one in-flight provider call — several editors opening the dashboard at once cost one provider request per widget, not one per user. This matters for rate-limited APIs (Umami Cloud allows 50 calls per 15 seconds; Fathom's API calls count against your pageview quota).
  • cache: false disables kv persistence entirely (the in-flight deduplication stays active). Only do this for debugging — caching is what protects the provider's rate limits.