Official Python SDK for the WireBoard REST and Live (SSE) APIs.
Project description
wireboard-api
Official Python 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 Python against.
- Sync and async client — pick whichever fits your app
- Strict type hints end-to-end via
TypedDict,Literal, and PEP 561 (py.typed) - One SSE engine, used by both managed and raw Live clients
- Built on
httpx+httpx-sse— works under asyncio, FastAPI, Django, Flask, scripts - Zero-config JWT rotation, drop-signal merging, hard-reconnect with snapshot refetch
Install
pip 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 os
import time
from wireboard_api import WireBoardClient
wb = WireBoardClient(token=os.environ["WIREBOARD_TOKEN"])
# Historical
sites = wb.sites()["sites"]
site = sites[0]
summary = wb.aggregate(
site_id=site["id"],
from_="2026-05-01",
to="2026-05-22",
)
print(f"{summary['visitors']} visitors, {summary['pageviews']} pageviews")
# Real-time (managed mode — SDK handles state, drop signals, JWT rotation)
live = wb.live(
site_id=site["id"],
categories=["visitors", "top_pages"],
)
live.subscribe(lambda state: print(
"now:", state["live"]["visitors"]["live"] if state["live"]["visitors"] else 0,
"top:", state["live"]["top_pages"][0]["url"] if state["live"]["top_pages"] else None,
))
live.start()
time.sleep(30)
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 is not next works as a change check.
Async
The same surface is available under AsyncWireBoardClient:
import asyncio
import os
from wireboard_api import AsyncWireBoardClient
async def main():
async with AsyncWireBoardClient(token=os.environ["WIREBOARD_TOKEN"]) as wb:
sites = (await wb.sites())["sites"]
summary = await wb.aggregate(
site_id=sites[0]["id"], from_="2026-05-01", to="2026-05-22",
)
print(summary)
asyncio.run(main())
API at a glance
Every method returns the unwrapped data payload from the API envelope and
raises WireBoardApiError / WireBoardAuthError on failure (see
Errors).
| Method | Returns | What it does |
|---|---|---|
account() |
Account |
Team-owner identity + the abilities of this token |
sites() |
SitesResult |
Every site owned by the team |
aggregate(...) |
AggregateResult |
Period totals (visitors, pageviews, bounce, duration) |
timeseries(...) |
TimeseriesResult |
One metric, bucketed by hour or day |
history(...) |
HistoryResult |
Visitors / returning / pageviews / bounce / duration per day |
breakdown(...) |
BreakdownResult |
Top-N rows by a single dimension |
urls(...) |
UrlsResult |
Per-URL metrics with prefix / contains / exact filters |
events(...) |
EventsResult |
Custom events report |
dimensions() |
Dimensions |
Meta: supported dimensions, metrics, limits |
live_state(...) |
LiveStateSnapshot |
Current per-category snapshot for one site |
live_token(...) |
LiveTokenResult |
Mint a 15-min subscriber JWT for the SSE stream |
live(...) |
LiveClient |
Managed Live client (handles snapshot + merge + rotation) |
live_raw(...) |
LiveRawClient |
Raw Live client (multi-site, custom merge) |
with_meta(fn) |
(data, rate_limit) |
Run a call and capture its rate-limit headers |
The async client (AsyncWireBoardClient) has identical method names with
async/await and AsyncLiveClient / AsyncLiveRawClient for Live.
Full reference: REST · Live · Errors.
Parameter conventions
- The wire-level
fromparameter is a Python reserved word. Pass it asfrom_=(trailing underscore — Python convention for keyword conflicts); the SDK strips the underscore at the wire boundary.to=is unchanged. - Date params accept a
YYYY-MM-DDstring, adatetime.date, or adatetime.datetime. Aware datetimes are converted to UTC; naive datetimes are taken as-is (passdatetime.now(timezone.utc), notdatetime.now(), if you want "today in UTC"). - Array params (e.g.
categories=["visitors", "top_pages"]) are comma-joined. - The
filterargument onevents()is serialised asfilter[<col>]=.../filter[props.<key>]=...automatically.
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
live = wb.live(
site_id="xK4mP2nT",
categories=["visitors", "top_pages", "active_sessions"],
on_change=lambda state: render(state["live"]),
on_error=lambda err: print(f"error: {err}"),
on_rotate=lambda: print("jwt rotated"), # optional, observability
on_reconnect=lambda: print("reconnected"), # optional, observability
)
live.start() # blocks until snapshot loaded + stream open
# state available at `live.state`; subscribe(...) returns an unsubscribe fn
What the managed client handles automatically:
- Fetches
/v1/live/stateon first connect and replays the snapshot. - Merges drop signals per category (
count: 0→ remove from top-N,step_count: 0→ remove fromactive_sessions). - Rotates the 15-minute JWT 60 s before expiry, with a 1 s zero-gap overlap between the old and new SSE connections — no event gap.
- Dedupes events by
lastEventIdacross the rotation boundary. - On a hard reconnect (connection drop before JWT expiry), waits 500 ms,
refetches the snapshot, mints a fresh JWT, and resumes — fires
on_reconnectonce so you can surface "silent recovery" in your UI.
on_rotate and on_reconnect are optional observability hooks; not
implementing them is the supported case for most apps.
Raw mode — multi-site, you own the state
def on_event(env):
# env["category"] is one of the 20 Live categories
if env["category"] == "top_pages":
for row in env["data"]:
# row["count"] == 0 means "remove from local state"
...
raw = wb.live_raw(
sites=["xK4mP2nT", "aB3cD4fG"],
categories=["visitors", "top_pages"],
on_event=on_event,
)
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.
Sync vs async Live
The sync LiveClient runs the SSE engine on a background asyncio event-loop
thread. Callbacks (on_change, on_event, ...) fire on that thread; use
threading primitives if your handler needs to mutate state shared with your
main thread.
The async AsyncLiveClient runs on the caller's event loop. Use it inside
FastAPI / Starlette / aiohttp / any asyncio app.
Types
The SDK ships strict type hints. Response types are TypedDicts, so dict
access works and your type checker can narrow safely:
from wireboard_api import WireBoardClient, AggregateResult
wb = WireBoardClient(token=token)
r: AggregateResult = wb.aggregate(site_id=sid, from_="2026-05-01", to="2026-05-22")
visitors: int = r["visitors"]
The Live envelope (LiveEnvelope) carries a category literal and a
loosely-typed data. Narrow with an if env["category"] == ... check:
def on_event(env: LiveEnvelope) -> None:
if env["category"] == "visitors":
print("live:", env["data"]["live"])
elif env["category"] == "top_pages":
for row in env["data"]:
print(row["url"], row["count"])
Errors
Four exception classes. WireBoardApiError is the base for everything
except auth failures; WireBoardAuthError is for genuine auth failures;
the other two are typed subclasses of WireBoardApiError for plan-gating
errors that deserve a distinct UX path.
from wireboard_api import (
WireBoardApiError,
WireBoardAuthError,
PaidPlanRequiredError,
PlanHistoryLimitExceededError,
)
try:
wb.aggregate(site_id=sid, from_=f, to=t)
except PlanHistoryLimitExceededError as err:
# 422 — free plan, `from_` is older than 30 days ago.
# err.earliest_allowed is 'YYYY-MM-DD'; retry with that, or prompt upgrade.
return wb.aggregate(site_id=sid, from_=err.earliest_allowed, to=t)
except PaidPlanRequiredError:
# 403 — endpoint requires a paid plan (currently the entire Live API).
# Auth is FINE; surface an upgrade prompt, do NOT push through re-login.
return show_upgrade_prompt()
except WireBoardAuthError as err:
# 401 → re-auth; 403 → re-mint a token with the right abilities.
print(err.http_status, err)
except WireBoardApiError as err:
if err.code == "site_not_found":
... # unknown site or wrong team
elif err.code == "concurrent_limit_reached":
... # too many live subscriptions
elif err.code == "unknown_filter":
... # events filter not whitelisted
# err.field_errors, err.http_status, err.rate_limit are on the error
PlanHistoryLimitExceededError and PaidPlanRequiredError both extend
WireBoardApiError, so existing except WireBoardApiError: blocks keep
working — order your handlers specific-to-general to leverage the
new types. PaidPlanRequiredError is deliberately NOT a
WireBoardAuthError even though its HTTP status is 403: the user's
authentication is fine, they just need to upgrade.
The SDK auto-retries once on a 429 (honouring Retry-After). Opt out
with WireBoardClient(token=token, retry_on_429=False). There are no
retries on 5xx or network errors — your code decides.
Cancellation
The HTTP client uses httpx under the hood. To cancel a long-running
async call, cancel the surrounding task — httpx propagates the cancel
into the open connection:
import asyncio
async def with_timeout():
try:
async with asyncio.timeout(5):
await wb.urls(site_id=sid, from_=f, to=t, prefix="/checkout")
except asyncio.TimeoutError:
...
The Live clients are cancelled via stop() instead — it also aborts the
in-flight snapshot fetch and JWT mint.
Rate-limit visibility
Every successful response carries X-RateLimit-* headers. To read them
without an extra HTTP call, wrap the request in with_meta:
data, rate_limit = wb.with_meta(
lambda c: c.aggregate(site_id=sid, from_=f, to=t),
)
print(f"{rate_limit['remaining']}/{rate_limit['limit']} requests left this minute")
with_meta is safe under concurrent use; each call captures its own slot.
Calls on the outer client (not the closure's c) are NOT instrumented.
In async code, the callback returns an awaitable:
data, rate_limit = await wb.with_meta(
lambda c: c.aggregate(site_id=sid, from_=f, to=t),
)
Browser examples
Four pages you can click through to see the SDK working against your real account. They double as a reference implementation of the production architecture: the Python SDK runs server-side, the browser only talks to your server, and the bearer token never leaves the host.
Browser ──HTTP/JSON──▶ Flask (scripts/) ──SDK──▶ api.wireboard.io
▲ ▼
└──── SSE ───────┘
(real-time state)
pip install -e ".[examples]" # adds flask + python-dotenv
WIREBOARD_TOKEN=… ./scripts/serve-examples.sh
The server binds to the first free port in 8080–8089 and prints the URL.
| Path | What it shows |
|---|---|
/account.html |
account() + sites() |
/historical.html |
7-day aggregate() + breakdown(country) + daily history() |
/live-managed.html |
A managed LiveClient per site, streamed to the browser over SSE — full merged state, drop signals applied, JWT rotation invisible |
/live-raw-multi.html |
A LiveRawClient across selected sites; per-site cards flash on each envelope, plus a rolling category-aware event log |
The token is loaded by the server from WIREBOARD_TOKEN or a .env file at
the repo root. Browsers see only short-lived data, never the bearer.
See scripts/README.md for the Flask app's endpoint
list and architecture notes.
Verify your setup
The package ships a CLI that exercises every endpoint against your real account:
WIREBOARD_TOKEN=… wireboard-api verify
It hits every REST surface for a 7-day window, opens a 45 s managed Live subscription, and prints a pass/fail summary table.
WireBoard SDK verify — token …62bb (sdk v1.0.0)
account() PASS team-owner: … abilities: analytics:read,live:read
sites() PASS 43 site(s); picked NxpRrJXr (analytics-alternative.com)
aggregate() PASS visitors=10 pageviews=22 bounce=70 dur=56s
…
live: stream (45s) PASS 237 events received
JWT rotation: not observed (need --duration=920 to verify)
reconnects: 0
errors: 0
──────────────────────────────────────
SUMMARY PASS 11/11 surfaces
| Flag | Default | Notes |
|---|---|---|
--token=TOKEN |
$WIREBOARD_TOKEN |
API bearer; flag wins over env. |
--site=SITE_ID |
first from /v1/sites |
Pin a specific site. |
--duration=SECONDS |
45 |
Live-stream window. Pass >=920 to also observe a full JWT rotation cycle (mints fire at expires_in − 60 s = 840 s). |
--no-color |
(auto) | Strip ANSI escapes; recommended for CI logs. |
Exit code is 0 on full pass, 1 on any failure, 2 on usage error
(unknown flag, malformed value).
Runtime targets
| Runtime | Tested | Notes |
|---|---|---|
| CPython 3.10–3.13 | ✓ | Reference target |
| PyPy 3.10+ | likely | Untested but uses no CPython-only APIs |
| Async runtimes | asyncio, Trio (via anyio) |
Whatever httpx supports |
| Frameworks | Django, Flask, FastAPI, Starlette, aiohttp | Just pip install wireboard-api |
The release was sanity-checked against production by running four
concurrent wireboard-api verify --duration=920 sessions in parallel —
one observed a graceful JWT rotation at +840.7 s (the spec'd
expires_in − 60 s), the other three exercised the hard-reconnect path
when prod-side infra closed the connection before rotation could fire.
Zero customer-visible errors across ~62 min of combined live streaming.
Contributing
git clone https://github.com/wireboard/api-py
cd api-py
python -m venv .venv && source .venv/bin/activate
pip install -e ".[dev]"
Quality gates (each one is also runnable in isolation):
pytest # 70 tests, ~17 s; includes an SSE stub-server
# integration suite for rotation + reconnect
mypy --strict wireboard_api # strict typing, no implicit Any
ruff check wireboard_api scripts/ tests/
To exercise the SDK against your real account:
WIREBOARD_TOKEN=… wireboard-api verify # 45 s smoke test
WIREBOARD_TOKEN=… wireboard-api verify --duration=920 # full JWT-rotation cycle
To bring up the Flask browser examples locally, add the examples extra:
pip install -e ".[examples]"
WIREBOARD_TOKEN=… ./scripts/serve-examples.sh
A .env file at the repo root (gitignored) is auto-loaded by both the CLI
helper and the example server.
More
License
MIT.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file wireboard_api-1.0.1.tar.gz.
File metadata
- Download URL: wireboard_api-1.0.1.tar.gz
- Upload date:
- Size: 38.3 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.10.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
3752802adf0dabbfed4420add222a24aa46bbf7e9e8ff4df377bbfc5cf56b41b
|
|
| MD5 |
100268ffaa58ec35bb52e3c1b62b18b3
|
|
| BLAKE2b-256 |
7a78a83f70f47305bc4ee1d9ac9f04ee4bc14a287001c8dfa122781c2fff37d5
|
File details
Details for the file wireboard_api-1.0.1-py3-none-any.whl.
File metadata
- Download URL: wireboard_api-1.0.1-py3-none-any.whl
- Upload date:
- Size: 49.8 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.10.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
101e1d007f37c769be80baa38adbec42083f611ae626b4dfd2f4f6368087ff4f
|
|
| MD5 |
129e8c8190d80a6998b5a0a95cde9ee2
|
|
| BLAKE2b-256 |
e7a9b5e9e0823a8a36b51eabb14a8a1ba3cb0bc60169150c7121588c06696585
|