Today WebView Bridge
Web SDK

API Reference

Full signatures for the @today/webview-bridge client.

createWebViewBridge

function createWebViewBridge(options?: WebViewBridgeOptions): WebViewBridge

Resolves the active native channel (window.webkit.messageHandlers.todayWebViewBridge on iOS, window.todayWebViewBridge on Android) and returns a client bound to it. If neither channel exists, the returned client is fully functional but inert — see safe degradation.

interface WebViewBridgeOptions {
  /**
   * Override the global lookup name. Defaults to "todayWebViewBridge".
   * Intended for staged channel migrations.
   */
  channelName?: string
  /**
   * Inject a channel directly, bypassing the window lookup.
   * Intended for tests and custom transports.
   */
  channel?: NativeChannel
}

isAvailable

isAvailable(): boolean

Returns true only when a Today native host channel is present. Use it to gate host-only features. It performs no I/O and is safe to call synchronously during render.

isAvailable() and platform are environment hints, not a security boundary — they read page-visible globals that page JS can spoof. Rely on them for rendering and feature-gating, never for a trust decision. See Not a security boundary.

platform

  readonly platform: Platform // 'ios' | 'android' | 'macos' | 'windows' | 'web'

The detected platform, resolved synchronously at createWebViewBridge() time — not a method, not a Promise. Safe to read during render. 'web' means no native host (the same condition as isAvailable() === false). See the Environment detection contract for how it is resolved and what the host must inject to split iOS from macOS.

if (bridge.platform === 'ios' || bridge.platform === 'macos') {
  // Apple-specific layout
}

track

track(event: string, properties?: TrackProperties): void

Reports an analytics event. Fire-and-forget: returns void, never throws, and must not be awaited. The host enriches the event with base fields and forwards it to PostHog.

type TrackProperties = Record<string, string | number | boolean>

properties accepts scalar values only. Nested array / object / null values are dropped by the host. The public (event, properties) arguments map onto the wire fields ename / parameters, which stay stable for the analytics pipeline. See the track contract.

getHeaders

getHeaders(): Promise<Record<string, string>>

Returns the headers the web must attach to its own fetch / XHR requests — Authorization plus any server-driven extra headers. Never rejects: resolves to {} when no host is present and on any transport fault. Callers (and the embed boot path) rely on this not throwing.

const headers = await bridge.getHeaders()
// { Authorization: "Bearer …", "X-Today-Extra": "…", … }

The result depends on the current page URL and login state. Outside a trusted feed domain, or when logged out, Authorization may be absent. Never assume it is present. See the headers contract.

refreshToken

refreshToken(): Promise<RefreshTokenResult>

Forces the host to mint a fresh access token. Call it when one of your own requests returns 401. Concurrent calls are coalesced by the host into a single real refresh, so it is safe to call from multiple in-flight requests at once.

interface RefreshTokenResult {
  authorization: string // ready-to-use header value, e.g. "Bearer …"
}

The result carries only authorization (a ready-to-use header value) — never the raw token. The SDK normalises the host reply to exactly this field and ignores any extras, keeping the secret surface minimal.

Rejects with one of three BridgeError types — distinguish them, because only one means the user is signed out:

RejectionCauseForce re-auth?
BridgeUnavailableErrorNo native hostNo — no host
BridgeErrorHost rejected (terminal auth failure)Yes
BridgeProtocolErrorHost replied with a malformed / empty resultNo — host bug

A BridgeProtocolError is a protocol/serialization bug on the native side, not a signed-out user — do not force re-authentication on it. Always wrap the call:

try {
  const { authorization } = await bridge.refreshToken()
  retryWith(authorization)
} catch (e) {
  if (e instanceof BridgeProtocolError) {
    // malformed reply — host bug, not a real auth failure; do not re-auth
  }
  // BridgeUnavailableError → no host; plain BridgeError → terminal auth failure
}

See the refreshToken contract and Error handling.

establishSession

establishSession(options?: { refresh?: boolean }): Promise<boolean>

Asks the host to mint the same-origin embed_bearer session cookie itself. The bearer is written into the WebView cookie store natively and is never returned to JS — the call resolves a plain boolean, not a token. Pass { refresh: true } to force a fresh server token before the mint (the 401 path).

Never throws. Resolves false with no host, an unsupported (older) host, or a failed mint — so the caller falls back to the header bootstrap:

// Preferred bootstrap: native mint, header exchange as the fallback.
if (!(await bridge.establishSession())) {
  await bootstrapFromHeaders() // getHeaders() → POST /api/auth/embed-session
}

// After a 401: force a refresh, then re-mint.
if (!(await bridge.establishSession({ refresh: true }))) {
  await refreshFromHeaders() // refreshToken() → POST /api/auth/embed-session
}

This is the hardened path: because the cookie is HttpOnly, the bearer never enters page JS at all. See the establishSession contract and the Security model.

fetch

fetch(input: RequestInfo | URL, init?: RequestInit): Promise<Response>

A convenience wrapper around the platform fetch that:

  1. calls getHeaders() and merges the result into the request headers (your explicit init.headers win over injected ones on this initial request);
  2. issues the request;
  3. if the response is 401 and the request is replayable, calls refreshToken() and retries once with the refreshed Authorization (which takes precedence over the caller header on the retry);
  4. returns the final Response.

A request is not replayable — and so the 401 is returned without a retry — when input is a Request object, or when init.body is a ReadableStream. Both are consumed by the first fetch and cannot be re-sent, so the SDK returns the 401 rather than throwing an opaque "body already used" error. For a retryable authenticated request, pass a URL string with a string / Blob / ArrayBuffer body.

When no host is present it is just fetch — no headers, no retry. This is the recommended entry point for authenticated requests; reach for the lower-level methods only when you need to control merging or retry yourself.

const res = await bridge.fetch('/feed/api/cards', { method: 'GET' })

Errors

All rejections are instances of BridgeError. The unavailable-host case is the BridgeUnavailableError subclass; a malformed host reply is the BridgeProtocolError subclass (a host bug, not a signed-out user). See Error handling for the full taxonomy.

On this page