i99dash docs
Reference

Raw host-bridge protocol

The window globals, calling conventions, and envelopes the SDK wraps — for debugging and for examples that call the bridge directly.

Prefer the SDK. MiniAppClient handles host resolution, schema validation, subscription routing, and timeouts — and bundles into a classic L5-safe IIFE. This page documents the wire so you can debug it, not so you should hand-roll it.

The SDK is a thin wrapper over window globals the host injects. Examples that call the bridge directly (and anyone debugging a "works on L8, not L5" report) need the exact contract below. Every fact here is the behaviour of the shipped SDK runtime.

Host globals and resolution order

The host injects the bridge on one of two globals. Resolution order matters — pick the branded one first, fall back to legacy, and require a callHandler function:

function resolveHost() {
  const branded = globalThis.__i99dashHost;          // HOST_GLOBAL
  if (branded && typeof branded.callHandler === 'function') return branded;
  const legacy = globalThis.flutter_inappwebview;    // LEGACY_HOST_GLOBAL
  if (legacy && typeof legacy.callHandler === 'function') return legacy;
  return null;
}

Binding to the legacy global when the branded one is present can resolve to a stale bridge whose v2 car.* handlers return nothing — the dashboard renders but every value stays . Match this order.

Push events arrive on globalThis.__i99dashEvents (HOST_EVENTS_GLOBAL). Subscribe with .on(channel, fn); the host calls .dispatch(channel, payload).

Two calling conventions

callHandler(name, payload) returns a Promise. The payload and response shape depend on the family.

Car family — flat payload

car.read, car.subscribe, car.identity, car.connection.subscribe:

// car.read — at most 64 names (CAR_MAX_NAMES)
const r = await host.callHandler('car.read', { names: ['battery_pct'] });
// success: { values: Record<string, number | null>, at: string }

// car.subscribe
const s = await host.callHandler('car.subscribe', {
  names: ['battery_pct'],
  idempotencyKey: crypto.randomUUID(),
});
// success: { subscriptionId: string, rejected?: string[] }

An error envelope is any object with an error key. Treat its presence as failure — do not read .values off it and silently render .

Native-capability families — enveloped payload

display.*, surface.*, cursor.*, gesture.*, pkg.*: wrap the request and unwrap the response:

const raw = await host.callHandler('display.list', {
  params: {},
  idempotencyKey: crypto.randomUUID(),
});
// { success: boolean, data?: unknown, error?: { code, message } }
if (raw && raw.success === false) throw new Error(raw.error.code);
const data = (raw && raw.data) || raw;

Push envelopes

__i99dashEvents.on('car.signal', (p) => {
  // { subscriptionId: string, data: { name: string, value: number | null, at: string } }
});
__i99dashEvents.on('car.connection', (p) => {
  // { subscriptionId: string, state: 'connected' | 'degraded' | 'disconnected' | 'unknown' }
});

Filter by subscriptionId if you hold more than one subscription on a channel.

Why the SDK is still the right call

It applies CAR_MAX_NAMES, throws typed errors instead of returning silent empties, parses responses against schemas, routes signals per subscriptionId, and resolves the host in the order above. Re-deriving all of that by hand is the most common source of the bugs this page exists to explain. It also bundles to a single classic IIFE, so the "raw keeps the bundle static" argument does not hold — see Build for L5 + L8.

On this page