Skip to content

Event payload reference

The wire format the Statable SDK uses to talk to the backend. Use it when you proxy events through your own domain, build a self-hosted ingester, or debug network traffic in DevTools.

The payload is intentionally compact (single-letter keys) to minimize bytes per request. Keys are stable across the v22 schema. New fields are added rather than renamed.


HTTP request

AspectValue
MethodPOST
URLdata-tracking-api attribute, or <script-origin>/api/event
Content-Typetext/plain
BodyJSON-encoded object (see schema below)
keepalivetrue (so unload events complete after navigation)
Cookiesnone. Visitor identity is server-generated (uid)

Content-Type: text/plain is intentional. Avoids a CORS preflight on cross-origin proxies and bypasses some ad-block heuristics targeting application/json.


Top-level fields

Appear on every event regardless of type.

FieldTypeAlwaysDescription
ustringyesCurrent URL (location.href)
inumberyesSite ID from data-id
rstring | nullyesdocument.referrer, or null if empty
vnumberyesSDK schema version (currently 22)
shstringyesHostname of the script source. Used to detect tampering / ad-block rewrites
nstringyesEvent name (see Event names)
pobjectoptionalCustom properties (data-statable-* + per-call props)
scnumberoptionalHTTP status code from <meta name="status:code">
uidstringoptionalVisitor ID (echoed back after the first server response)

Event names (n)

The n field selects how the backend processes the payload.

n valueMeaning
pageviewStandard pageview, fired on load + SPA navigation
engagementSession-end summary (time on page, scroll depth)
[Custom Event]Any name from window.statable.t(...) or data-statable-event, wrapped in brackets
[Outbound Link Click]Auto-tracked external link click (also bracket-wrapped)
[File Download]Auto-tracked download click (also bracket-wrapped)
"" (empty)Heartbeat. Visibility ping, not a discrete event

The bracket wrapping ([Sign Up]) lets the backend distinguish system events (pageview, engagement) from everything else without a separate field. All custom events, yours and the SDK's auto-tracked ones, go through the same wrapper.


Pageview payload

{
  "u": "https://example.com/pricing",
  "i": 12345,
  "r": "https://google.com",
  "v": 22,
  "sh": "statable.com",
  "n": "pageview",
  "p": { "cohort": "beta", "userId": "u_42" },
  "sc": 200
}

p is present only when there are sticky data-statable-* attributes or a data-before-send hook adds props.

sc is present only when a <meta name="status:code"> tag is on the page (typically for 404 / 410 templates).


Engagement payload

Sent on beforeunload, pagehide, tab-hidden, or after a 28-minute idle timeout. Carries the totals the dashboard needs for time-on-page and scroll-depth charts.

{
  "u": "https://example.com/pricing",
  "i": 12345,
  "r": "https://google.com",
  "v": 22,
  "sh": "statable.com",
  "n": "engagement",
  "sd": 75,
  "e": 45000,
  "d": 12000,
  "h": 1,
  "tt": 60123,
  "to": 1
}
FieldTypeDescription
sdnumberMax scroll depth reached on this page, in % (0–100)
enumberTotal engagement time in milliseconds (active tab only)
dnumberDelta since the last heartbeat (ms), for incremental aggregation
h1Marker. Engagement summary, not a heartbeat
ttnumberTotal elapsed time since page load (performance.now(), ms)
to1Present only when fired due to the 28-minute idle timeout

Defensive drop: the SDK skips engagement events where e < 2000ms and scroll depth is 0. Guards against bounce noise from prefetch / preview.


Heartbeat payload

Sent on visibility changes (tab hide / show) once a pageview has been recorded. Tiny and frequent. Keeps session timing accurate even when the user is off-tab.

{
  "n": "",
  "u": "https://example.com/pricing",
  "i": 12345,
  "hb": 1,
  "vis": 0,
  "e": 32000,
  "d": 8000,
  "v": 22
}
FieldTypeDescription
hb1Heartbeat marker
vis0 | 11 = tab became visible, 0 = tab became hidden
enumberCumulative engagement so far (ms)
dnumberDelta since the previous heartbeat (ms)

Heartbeats deliberately omit r, sh, p. The backend has those from the prior pageview.


Custom event payload

window.statable.t('Purchase', { plan: 'pro', amount: 49 }) produces:

{
  "u": "https://example.com/checkout/success",
  "i": 12345,
  "r": "https://example.com/checkout",
  "v": 22,
  "sh": "statable.com",
  "n": "[Purchase]",
  "p": { "plan": "pro", "amount": 49 }
}

Note the brackets around the name. They're added automatically. Don't pre-bracket your event names.


Outbound / file payload

{
  "n": "[Outbound Link Click]",
  "u": "https://example.com/blog/post",
  "i": 12345,
  "v": 22,
  "sh": "statable.com",
  "p": { "url": "https://github.com/statable" }
}

For File Download, only the n value differs ([File Download]). The p.url field carries the link target with the query string stripped, which keeps download URLs aggregable.


Server response

The backend responds with a small JSON body:

{ "uid": "v_a8b2c91e..." }

The SDK caches uid in memory (not in cookies or localStorage) and echoes it back on subsequent requests as the top-level uid field. Stitches events from a single page session without persistent client-side identifiers.

The uid is intentionally per-tab, per-session. Not a cross-session visitor ID. That's computed server-side from anonymized signals.


Bot detection

Two layers reject bot traffic.

Client-side (drops before send):

  • navigator.webdriver === true
  • window.Cypress defined
  • localStorage['analytics_ignore'] === 'true' (manual opt-out)

Server-side (drops after receive):

  • Multiple User-Agent parsers cross-check the request UA.
  • IP / ASN heuristics for known crawlers and datacenter ranges.
  • Unrealistic engagement / pageview ratios are filtered from reports.

Bot-flagged events are stored but excluded from default dashboard metrics.


Schema versioning

v is the schema version. Today: 22. New optional fields may be added in minor versions. Existing field names and types don't change without a major version bump. Custom proxies should forward unknown fields rather than reject them. The backend tolerates extras.

If you build a self-hosted ingester, treat the bundles served from statable.com as the source of truth. The HTTP body shape is what the live SDK actually emits at any point in time.


See also


Ready to take control of your web analytics? Try Statable free for 30 days — no credit card required, full feature access, GDPR-compliant by default. Start your free trial or view a live demo.