Skip to content

wireboard/api-js

Repository files navigation

WireBoard

@wireboard/api

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.

npm version bundle size (minified + gzipped) types: TypeScript license: MIT


  • 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

Install

npm install @wireboard/api

Quickstart

Mint 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.

API at a glance

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.

Live API: two modes

The SDK exposes both a managed and a raw client over the same SSE protocol. Pick based on what your UI needs.

Managed mode — single site, SDK owns the state

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 fn

The 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.

Raw mode — multi-site, you own the state

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.

TypeScript: precise types per request

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.

Browser usage

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 opens wb.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 one wb.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.

Errors

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.

Cancellation

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.

Rate-limit visibility

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.

Verify your setup

The package ships a CLI that exercises every endpoint against your real account:

WIREBOARD_TOKEN=… npx @wireboard/api verify

It 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 targets

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.

Contributing

git clone https://github.com/wireboard/api-js
cd api-js
npm install
npm run build && npm test

To 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.sh

The 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.

More

License

MIT.

About

Official TypeScript SDK for the WireBoard REST and Live APIs. Browser + Node, fully typed.

Topics

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors