Authentication
Send your organization API key as a bearer token on every request. Keys are created in VideoBI under Settings → API keys by an owner or admin, and look like vai_key_….
Authorization: Bearer vai_key_…- Base URL:
https://app.videobi.ai - Keys are read-only. A key limited to some locations sees only those locations' data, and dashboards scoped to them or to every location.
- Call the API from your server. It sends no CORS headers, so browser pages can't call it directly.
- Revoking a key in Settings stops it at once.
List dashboards
GET /v1/api/dashboardsEvery published dashboard the key can see.
{
"items": [
{ "id": "43009756-579f-45ea-b0dd-9ed0a10f0bc8", "name": "Area manager weekly",
"description": "Every store against the chain, by district: …",
"audience": "manager", "cadence": "weekly", "version": 1, "published_at": "2026-10-01T18:29:07.406Z" }
]
}audience is executive, manager or frontline. version increases each time the dashboard is republished.
Get a dashboard with its data
GET /v1/api/dashboards/{id}?range=last_7_days| Parameter | What it does |
|---|---|
range | A time range, relative to now in the location's time zone. One of today, yesterday, last_24_hours, last_7_days, this_week, last_week, last_4_weeks, last_30_days, this_month, last_month, last_90_days, this_quarter, last_quarter, last_13_weeks, last_12_months. Leave it out to use the dashboard's own default. |
from, to | A custom window instead of range: ISO 8601 with a time-zone offset, like 2026-09-01T00:00:00-07:00. Send both, or neither. |
location | One location's id: the dashboard for that store or site only. Without it, every location the key can see. |
daypart | A daypart name, like lunch_peak, to limit hours where the widgets support it. |
The response
{
"api_version": 1,
"dashboard": { "id": "…", "name": "Area manager weekly", "description": "…", "audience": "manager",
"cadence": "weekly", "version": 1, "published_at": "…", "columns": 12 },
"time_range": { "preset": "last_7_days" },
"timezone": "America/Los_Angeles",
"filters": { "location_ids": null, "daypart": null },
"rendered_at": "2026-10-06T22:07:34.448Z",
"widgets": [
{
"id": "waste", "title": "Waste", "viz": "stat",
"layout": { "x": 0, "y": 0, "w": 3, "h": 3 },
"metric": { "id": "waste_value", "name": "Waste", "description": "What the discarded items would have sold for.",
"unit": "currency", "format": "currency", "direction": "lower_is_better" },
"query": { "metric": "waste_value", "grain": "period", "group_by": [], "compare_to": "prior_period", … },
"display": { "format": "currency", "aggregate": "sum" },
"data": {
"columns": [ { "name": "bucket", "type": "timestamp" }, { "name": "value", "type": "currency" },
{ "name": "coverage_pct", "type": "ratio" }, { "name": "compare_value", "type": "currency" }, … ],
"rows": [ ["2026-09-29T07:00:00.000Z", 1840.5, 0.97, 2010.25, -169.75, "…"] ],
"coverage": { "overall": 0.97, "byRow": [0.97] },
"meta": { "from": "…", "to": "…", "grain": "period", "timezone": "…", "warnings": [] }
}
}
]
}| Widget field | Meaning |
|---|---|
viz | How the app draws it: stat, line, bar, area, table, gauge, heatmap and more. Rendering maps each to Apache ECharts. |
layout | Position on a grid of dashboard.columns (12) columns: x, y in cells from the top left, w, h in cells. |
metric | What is measured, its unit and format, and whether higher or lower is better. null for widgets that aren't a metric (images, alerts). |
query | What was asked: the metric, the time grain (hour, day, week, month, quarter, or period for one total), the dimensions it is split by (group_by) and any comparison (compare_to). |
display | Presentation hints: number format, alert thresholds, sort order, reference lines. |
data | The result. For metric widgets: columns (name and type), rows as arrays in column order, coverage (how much of the time the cameras could see, 0–1) and meta; assets for image and clip widgets, with links valid for 10 minutes; or {"error": {"code", "message"}} when that one widget couldn't be computed. One failed widget never fails the request. |
Examples
Node.js
const res = await fetch("https://app.videobi.ai/v1/api/dashboards/DASHBOARD_ID?range=last_30_days", {
headers: { Authorization: `Bearer ${process.env.VIDEOBI_KEY}` },
});
if (!res.ok) throw new Error(`VideoBI ${res.status}: ${(await res.json()).detail}`);
const dashboard = await res.json();
for (const w of dashboard.widgets) console.log(w.title, w.viz, w.data.rows?.length ?? 0, "rows");Python
import os, requests
r = requests.get(
"https://app.videobi.ai/v1/api/dashboards/DASHBOARD_ID",
params={"range": "last_30_days"},
headers={"Authorization": f"Bearer {os.environ['VIDEOBI_KEY']}"},
timeout=30,
)
r.raise_for_status()
for w in r.json()["widgets"]:
print(w["title"], w["viz"], len(w["data"].get("rows", [])), "rows")Errors
Errors use problem details (application/problem+json): {"type", "title", "status", "detail"}, with detail written for a person.
| Status | When |
|---|---|
400 | A parameter isn't valid: an unknown range, from without to, both range and from. |
401 | No key, or a key that is revoked, expired or mistyped. |
403 | The plan doesn't include the API, or the key isn't allowed that location. |
404 | No published dashboard with that id that this key can see. |
429 | More than 60 requests in a minute with one key. Wait a minute and retry. |