Capabilities
The normative request and result of track, headers, refreshToken, and establishSession.
Three capabilities ship in v1 — track, headers, refreshToken.
establishSession is the additive hardening capability layered on top (see
Versioning). Each entry below is normative.
track
Report an analytics event. Fire-and-forget.
The web SDK exposes this as track(event, properties?). The wire payload keeps
the analytics-pipeline field names: the SDK maps track(event, properties)
onto { type: 'track', ename: event, parameters: properties }. The public
parameter names (event / properties) and the wire fields (ename /
parameters) are intentionally distinct.
| Direction | web → native |
| Request | { type: 'track', ename: string, parameters?: TrackProperties } |
| Result | undefined |
| Awaited by web | No |
| May reject | No |
type TrackProperties = Record<string, string | number | boolean>Request (wire fields)
ename— the event name (public API:event). Required, non-empty string.parameters— optional flat map of scalar values (public API:properties).
Host behaviour
-
The host MUST enrich the event with its base fields before forwarding to PostHog. Base fields are owned by the host, not the web:
Field Meaning eidevent id sidsession id tstimestamp tztimezone uiduser id diddevice id client_platclient platform ( ios/android/ …)ctxhost-defined context blob -
The host MUST accept scalar
parametersvalues (string/number/boolean) and MUST drop non-scalar values (array/object/null). The analytics pipeline does not consume nested structures. -
The host SHOULD treat delivery as best-effort and MUST NOT require the web to await. The web side resolves immediately regardless of delivery.
-
The host MUST serve
trackto the main frame only and MUST ignore atrackfrom a cross-origin sub-frame, so an untrusted frame cannot forge host-enriched analytics events.
Web behaviour
- The web MUST NOT await
trackin a way that blocks UI, and MUST swallow any rejection (so a stray rejection never becomes anunhandledrejection).
headers
Return the headers the web must attach to its own fetch / XHR requests.
| Direction | web → native |
| Request | { type: 'headers' } |
| Result | Record<string, string> |
| Awaited by web | Yes |
| May reject | No (resolves {} when nothing to inject) |
Host behaviour
- The result is the set of headers the web SHOULD attach to its own requests:
the
Authorizationheader plus any server-driven extra headers the host has been told to forward. - The result MUST be a flat
string → stringmap. Authorization(the bearer) MAY be absent. It is exposed only on an HTTPS trusted surface — a feed / embed surface or an auth-whitelisted scope, reached overhttps. A cleartexthttpURL, an untrusted domain, a not-yet-committed page, or a logged-out session MUST yield a map withoutAuthorization(possibly{}). Server-driven non-secret headers (e.g.feedExtraHeaders) are gated on the trusted surface but are not HTTPS-restricted — only the bearer is.- The host MUST NOT reject this capability under normal operation; "nothing
to inject" is expressed as
{}, not an error. headersis a token-bearing capability and is main-frame only. The host MUST serve it to the top-level document frame only; a call originating from a cross-origin sub-frame MUST resolveundefined(the SDK then degrades to{}). A token MUST NOT be handed to a sub-frame.
Why this capability exists
A WebView host can only decorate the top-level document navigation request. The
SPA's own fetch / XHR are subresource requests the host cannot intercept or
rewrite. So the host cannot push Authorization onto them — instead it hands the
headers to the web, which attaches them itself. See the
Introduction.
Prefer
establishSessionwhere the host supports it. It mints the session cookie natively so the bearer never enters page JS;headers(returningAuthorizationfor the web to exchange at/api/auth/embed-session) is the fallback for hosts that don't implement it. See the Security model.
refreshToken
Force the host to mint a fresh access token.
| Direction | web → native |
| Request | { type: 'refreshToken' } |
| Result | { authorization: string } |
| Awaited by web | Yes |
| May reject | Yes (terminal auth failure) |
interface RefreshTokenResult {
authorization: string // ready-to-use header value, e.g. "Bearer <token>"
}When the web calls it
After one of the web's own authenticated requests returns 401. The web then
retries that request once with the refreshed authorization.
Host behaviour
- The host MUST coalesce concurrent
refreshTokencalls into a single real refresh. Many in-flight requests hitting401at once MUST NOT cause multiple refresh round-trips. This shares the same coalescing path as the native API client's own401handling. - On success the host MUST resolve with a ready-to-use
authorizationheader value (e.g.Bearer …) and MUST NOT include the raw token. The web side consumes onlyauthorization; the SDK normalises the reply to that one field and ignores any extras. - On terminal failure (refresh token expired/revoked, network-terminal, signed out) the host MUST reject with an error message — see Error handling.
refreshTokenis token-bearing and carries the same trust gate as theheadersbearer: it is served only to the main frame on an HTTPS trusted surface. A sub-frame, a cleartext page, or any main-frame URL that is not a trusted surface MUST resolveundefined— a token MUST NOT be minted. (The SDK maps thatundefinedtoBridgeProtocolError, not a terminal auth failure — see Error handling.)
Web behaviour
- The web MUST wrap
refreshTokenintry/catchand MUST NOT retry the original request more than once. A second401after a fresh token is a real authorization failure, not a stale token.
establishSession
Ask the host to (re)mint the same-origin session cookie itself. The bearer is
written into the WebView cookie store natively and is NEVER returned to JS —
the reply carries only { ok: boolean }.
| Direction | web → native |
| Request | { type: 'establishSession', refresh?: boolean } |
| Result | { ok: boolean } (SDK returns boolean) |
| Awaited by web | Yes |
| May reject | No (failure is { ok: false }, or undefined) |
Request
refresh— optional boolean, defaultfalse. Whentrue, the host MUST force a fresh server token refresh before minting the cookie — this is the401path. Whenfalseor absent, the host MAY mint from the token it already holds.
Host behaviour
- The host MUST mint (or refresh) the same-origin
embed_bearercookie into the WebView cookie store itself and MUST NOT return the bearer to JS. The cookie MUST beHttpOnly,Secure, and scopedPath=/api/, so it rides every same-origin request — including the dynamicimport()of widget bundles the browser's module loader sends without a headers API. This is the same cookie the header bootstrap mints via/api/auth/embed-session; here the host mints it directly. refresh: trueMUST force a token refresh first and MUST share therefreshTokencoalescing path, so concurrent401s collapse into one real refresh. SeerefreshToken.establishSessionis token-bearing and carries the same trust gate as theheadersbearer: it is served only to the main frame on an HTTPS trusted surface. A cross-origin sub-frame, a cleartexthttppage, or a main-frame URL that is not a trusted surface MUST resolveundefined— no cookie is minted.- On success the host resolves
{ ok: true }; on a mint failure it resolves{ ok: false }. It MUST NOT reject — the caller falls back to the header bootstrap on anything other than{ ok: true }.
Why the bearer is never returned
Because the host writes an HttpOnly cookie, the bearer never lands in a place
page JS can read — not in a variable, not in storage, not on the cookie it can
document.cookie-inspect. This closes the "a main-frame script could read the
bearer" caveat that the headers path carries by construction. It is the
preferred path on a conforming host; see the
Security model.
Web behaviour
- The SDK exposes this as
establishSession(options?), returningPromise<boolean>. It resolvesfalse— never throws — with no host, an unsupported host, or a failed mint. - An older host that does not recognise the
typeresolvesundefined, which the SDK maps tofalse, so the caller transparently falls back to the header-based bootstrap. This is the Versioning forward-compatibility rule in action.