Official TypeScript SDK for the WireBoard REST and Live APIs.
Pull historical analytics, subscribe to real-time visitor activity, and integrate WireBoard with anything you can write code against.
- Works in modern browsers and Node 18+
- ESM and CJS dual-published, separate tree-shakeable browser bundle
- Browser bundle under 15 KB gzipped, zero runtime dependencies in the browser
- Strict TypeScript types end-to-end, including discriminated unions for Live events and per-dimension typing for breakdowns
npm install @wireboard/apiMint a token in Settings → API on your WireBoard dashboard (needs the
analytics:read ability for REST, live:read for the Live API). Then:
import { WireBoardClient } from '@wireboard/api';
const wb = new WireBoardClient({ token: process.env.WIREBOARD_TOKEN! });
// Historical
const { sites } = await wb.sites();
const site = sites[0]!;
const summary = await wb.aggregate({
site_id: site.id,
from: '2026-05-01',
to: '2026-05-22',
});
console.log(`${summary.visitors} visitors, ${summary.pageviews} pageviews`);
// Real-time (managed mode — SDK handles state, drop signals, JWT rotation)
const live = wb.live({
siteId: site.id,
categories: ['visitors', 'top_pages'],
});
live.subscribe(state => {
console.log(
'now:', state.live.visitors?.live ?? 0,
'top:', state.live.top_pages[0]?.url,
);
});
await live.start();
// later:
// live.stop();The SDK handles snapshot rebuild on reconnect, drop signals, and short-lived
JWT rotation for you. A NEW state object is emitted on every update, so
prev !== next works as a change check in React, Vue, or Svelte.
Every method returns the unwrapped data payload from the API envelope and
throws WireBoardApiError / WireBoardAuthError on failure (see
Errors). Every method accepts an optional { signal?: AbortSignal }
as a final argument.
| Method | Returns | What it does |
|---|---|---|
account() |
Account |
Team-owner identity + the abilities of this token |
sites() |
SitesResult |
Every site owned by the team |
aggregate(params) |
AggregateResult |
Period totals (visitors, pageviews, bounce, duration) |
timeseries(params) |
TimeseriesResult |
One metric, bucketed by hour or day |
history(params) |
HistoryResult |
Visitors / returning / pageviews / bounce / duration per day |
breakdown<D>(params) |
BreakdownResult<D> |
Top-N rows by dimension; row type is narrowed by D |
urls(params) |
UrlsResult |
Per-URL metrics with prefix / contains / exact filters |
events<G>(params) |
EventsResult<G> |
Custom events report; row type is narrowed by group_by |
dimensions() |
Dimensions |
Meta: supported dimensions, metrics, limits |
liveState(params) |
LiveStateSnapshot |
Current per-category snapshot for one site |
liveToken(params?) |
LiveTokenResult |
Mint a 15-min subscriber JWT for the SSE stream |
live(options) |
LiveClient |
Managed Live client (handles snapshot + merge + rotation) |
liveRaw(options) |
LiveRawClient |
Raw Live client (multi-site, custom merge) |
withMeta(fn) |
{ data, rateLimit } |
Run a call and capture its rate-limit headers |
Full reference: REST · Live · Errors.
The SDK exposes both a managed and a raw client over the same SSE protocol. Pick based on what your UI needs.
const live = wb.live({
siteId: 'xK4mP2nT',
categories: ['visitors', 'top_pages', 'active_sessions'],
onChange: state => render(state.live),
onError: err => console.error(err),
onRotate: () => log('jwt rotated'), // optional, observability
onReconnect: () => log('reconnected'), // optional, observability
});
await live.start();
// state available at `live.state`; subscribe(...) returns an unsubscribe fnThe managed client fetches /v1/live/state on connect, merges drop signals
per category (count: 0 → remove from top-N, step_count: 0 → remove from
active_sessions), rotates the JWT 60 s before expiry with a zero-gap
overlap, dedupes events by lastEventId, and refetches the snapshot on
hard reconnect. onRotate and onReconnect are optional observability
hooks for tracking transparent recoveries.
const raw = wb.liveRaw({
sites: ['xK4mP2nT', 'aB3cD4fG'],
categories: ['visitors', 'top_pages'],
onEvent: env => {
// env is a discriminated union — TS narrows env.data by env.category
if (env.category === 'top_pages') {
for (const row of env.data) {
// row.count === 0 means "remove from local state"
}
}
},
});
await raw.start();Use raw mode for multi-site dashboards, when you already have your own reactive store, or when you want full control over how drop signals apply.
The generic methods narrow row shapes based on the request.
// Dimension narrows the row's per-dimension field:
const c = await wb.breakdown({ site_id, from, to, dimension: 'country' });
c.rows[0]; // { country: string; visitors: number }
const m = await wb.breakdown({ site_id, from, to, dimension: 'ref_medium' });
m.rows[0]; // { medium: string; visitors: number }
// group_by narrows event row keys (cast with `as const` for the tightest types):
const r = await wb.events({
site_id, from, to,
group_by: ['category', 'utm_source'] as const,
});
r.rows[0]; // { category: string | null; utm_source: string | null; count: number; value: number }The Live envelope is a discriminated union: switch (env.category) narrows
env.data to the right shape automatically — no casts needed.
Bundlers (Vite, Webpack, esbuild, Rollup) pick the browser-targeted ESM build automatically via the package's conditional exports — no config needed:
// React / Vue / Svelte / vanilla — same import, browser ESM build resolved
import { WireBoardClient } from '@wireboard/api';
const wb = new WireBoardClient({ token: yourShortLivedTokenFromYourServer });The long-lived bearer token does not belong in untrusted contexts. The right architecture depends on your audience:
-
Internal pages (team views, ops displays, admin dashboards behind your own auth): use the SDK directly in the browser with the two-token flow. Your server mints a short-lived subscriber JWT via
liveToken(), the browser openswb.live(...)with it. The JWT is scoped to specific sites + categories and expires in 15 minutes, so it's safe in bounded-audience client code. -
Public-facing pages: don't put either the bearer or a JWT in the browser. Use the SDK on your backend to either poll
liveState()and serve a cached snapshot, or hold onewb.live(...)connection and fan out updates to your visitors via your own SSE or WebSocket. See Scaling browser subscribers in the docs for the three patterns and which to pick.
Working browser demos in examples/browser/
cover the internal-page case — four zero-build pages covering REST,
historical analytics, and both Live modes. See
examples/ for the full set.
Two error classes, both extend Error:
import { WireBoardApiError, WireBoardAuthError } from '@wireboard/api';
try {
await wb.aggregate({ site_id, from, to });
} catch (err) {
if (err instanceof WireBoardAuthError) {
// 401 → re-auth; 403 → re-mint a token with the right abilities
} else if (err instanceof WireBoardApiError) {
switch (err.code) {
case 'site_not_found': /* unknown site or wrong team */ break;
case 'concurrent_limit_reached': /* too many live subscriptions */ break;
case 'unknown_filter': /* events filter not whitelisted */ break;
// ...
}
// err.fieldErrors, err.httpStatus, err.rateLimit are all on the error
}
throw err;
}The SDK auto-retries once on a 429 (honouring Retry-After). Opt out
with new WireBoardClient({ token, retryOn429: false }). There are no
retries on 5xx or network errors — your code decides.
Every REST call accepts an AbortSignal via a { signal } second argument
(standard fetch idiom):
const controller = new AbortController();
const promise = wb.urls(
{ site_id, from, to, prefix: '/checkout' },
{ signal: controller.signal },
);
// elsewhere — e.g. component unmount, route change, user cancel
controller.abort();The Live clients are cancelled via .stop() instead — it also aborts any
in-flight snapshot fetch or JWT mint.
Every successful response carries X-RateLimit-* headers. To read them
without an extra HTTP call, wrap the request in withMeta:
const { data, rateLimit } = await wb.withMeta(c => c.aggregate({
site_id, from, to,
}));
console.log(`${rateLimit?.remaining}/${rateLimit?.limit} requests left this minute`);withMeta is safe under Promise.all; each call captures its own slot.
Calls on the outer client (not the closure's c) are NOT instrumented.
The package ships a CLI that exercises every endpoint against your real account:
WIREBOARD_TOKEN=… npx @wireboard/api verifyIt hits every REST surface for a 7-day window, opens a 45-second managed
Live subscription, and prints a pass/fail summary table. Exit code 0 on
full pass, 1 on any failure, 2 on usage error. Use --no-color for CI
logs and --duration=920 to also observe a full JWT rotation cycle.
| Runtime | Build picked | Notes |
|---|---|---|
| Bundler (Vite, Webpack, esbuild, Rollup) | dist/index.browser.js (ESM) |
Uses global EventSource; no eventsource polyfill bundled |
Node ESM ("type": "module") |
dist/index.js (ESM) |
Pulls in eventsource for SSE |
Node CJS (require(...)) |
dist/index.cjs (CJS) |
Same as Node ESM |
| TypeScript | dist/index.d.ts / .d.cts |
Resolution matches the runtime build |
You don't configure anything — import { WireBoardClient } from '@wireboard/api'
just works in every environment.
git clone https://github.com/wireboard/api-js
cd api-js
npm install
npm run build && npm testTo exercise the browser examples against the packed tarball (the same
shape users get from npm install), put a token in .env at the repo
root and run:
WIREBOARD_TOKEN=… ./scripts/test-examples.shThe script builds, runs npm pack, installs the tarball into a scratch
directory, transforms each examples/browser/*.html to import the local
bundle, and serves everything on a free port in 8080–8089. Ctrl+C cleans
up. This is the closest you can get to a customer install without
publishing — use it before sending a PR that touches the build, the
public API surface, or any browser example.
MIT.