Analytics Export API
The Analytics Export API gives programs — a script, a data warehouse job, or a BI tool — direct access to the same Beezi and Claude Code analytics you see on the dashboards. A program authenticates with an API key and pulls the numbers as JSON, so you can build your own reports, alerts, and spreadsheets on top of Beezi data.
It is a read-only export surface: every figure comes from the exact same server that powers the in-app charts, so what you export always matches what you see.
Pricing & Availability
Analytics export is part of the analytics feature and requires a plan that includes analytics — the same paid gate as the dashboards.
Who can manage keys depends on the tenant type (see Tenant types):
- Standard tenants — Tenant Owner and Admin.
- Analytics tenants — Tenant Owner, Admin, and Supervisor. A supervisor's key reads only the groups they supervise.
1. Getting an API key
API keys are managed in the app under Settings → Connections → API Keys. The card is visible only to the roles listed above.
Where to find it
On the API Keys card, open the ⋮ menu and choose Manage. The dialog shows the keys you own and lets you add or revoke them.
Creating a key
- In the Manage API keys dialog, click Add key.
- Optionally enter a label (default
Analytics API Key) to recognize the key later. - Click Create key.
- Copy the secret shown on the next screen.
The full secret is shown only once, immediately after creation. Copy it and store it somewhere safe — it is never displayed again and is not stored by Beezi. If you lose it, revoke the key and create a new one.
How many keys you can have
You can have up to 5 active keys per user. The limit counts the keys you created, not the whole tenant — two admins each get their own 5. At your limit, creating another key is rejected until you revoke one. You also see and manage only your own keys: an admin cannot revoke a key created by someone else.
Key statuses
| Status | Meaning |
|---|---|
| Active | Live and usable. Counts toward your 5-key limit. |
| Pending | A slot reserved while the key is being created (transient — you rarely see it). |
| Revoked | Turned off. Frees a slot and stops working immediately. |
Revoking and rotating
Open Manage API keys, use a key's ⋮ menu, and choose Revoke (with confirmation). Revoking blocks the key immediately and frees one of your slots. To rotate a key safely: create a new one, switch your integration to it, then revoke the old one. If a secret may have leaked, revoke it right away and create a replacement.
A key remembers who created it. If that admin's account is later deleted, the key keeps working but drops out of everyone's management list because it no longer has an owner. Revoke keys you no longer need before offboarding the admin who owns them.
2. Tenant types
Beezi has two kinds of tenant, and they export differently. If you are not sure which you have, the fastest test is to call POST /beezi once — an analytics tenant answers 404.
| Standard tenant | Analytics tenant | |
|---|---|---|
POST /beezi | available | 404 — cost analytics do not exist here |
POST /claude-code | available | available |
| Organized by | projects and repositories | user groups |
| Filters | userEmail, projectName, repository | userEmail, groupName |
| Data you receive | the whole tenant | scoped to your role (below) |
An analytics tenant connects no repositories and tracks no tickets, so every Beezi dataset (budgets, cost attribution, task charts) is meaningless there. Rather than return empty objects, the endpoint reports that it does not exist.
Data scope on analytics tenants
On an analytics tenant, a key reads only what its creator is allowed to see:
| Creator's role | Reads |
|---|---|
| Tenant Owner, Admin | every member of the tenant |
| Supervisor | every member of the groups they supervise |
Add filters.groupName to narrow further. Owners and admins may name any group; a supervisor may name only a group they supervise.
Scope is recalculated from the creator's current roles on every request, not frozen when the key was made. If someone stops supervising a group, their existing keys stop returning that group's data on the very next call — there is nothing to rotate. Likewise, deactivating a person or removing their seat immediately disables every key they created.
A key whose scope covers nobody — a supervisor with no groups, for example — returns an ordinary empty result, not an error.
3. Authentication
Send the key secret as a Bearer token on every request:
Authorization: Bearer <your-secret>
Each key is issued with the scope analytics:export and two claims that identify where it may read:
{ "tenantId": "<tenant id>", "internalUserId": "<creator user id>" }
On every request Beezi verifies the key, confirms it carries both claims and the scope, and reads the tenant from the key.
The tenant an export reads is taken only from the key — you can never pass a tenant or user id in the request. That is how one tenant's key can never reach another tenant's data.
Authentication failures:
- 401 Unauthorized — the key is missing, malformed, invalid, revoked, or expired.
- 403 Forbidden — the key is valid but missing a required claim or the scope; the tenant's plan does not include analytics; or the person who created the key has been deactivated or has lost their seat.
4. Making a request
Both endpoints accept a JSON body and return JSON.
POST https://<your-host>/api/v1/analytics-export/beeziPOST https://<your-host>/api/v1/analytics-export/claude-code
{
"type": "sessions",
"dateRange": {
"from": "2026-01-01T00:00:00Z",
"to": "2026-02-01T00:00:00Z"
},
"filters": {
"userEmail": "jane",
"projectName": "checkout",
"repository": "acme/api"
},
"limit": 100,
"cursor": "..."
}
On an analytics tenant the same request uses the group dimension instead:
{
"type": "sessions",
"dateRange": {
"from": "2026-01-01T00:00:00Z",
"to": "2026-02-01T00:00:00Z"
},
"filters": {
"userEmail": "jane",
"groupName": "Engineering"
},
"limit": 100
}
Field rules:
type(required) — the dataset name (see Datasets).dateRange(required) — ISO 8601 UTC.fromis inclusive,tois exclusive,frommust be beforeto, and the span may not exceed 366 days.filters(optional) — see Filters.limit(optional) — rows per page, default 100, max 1000. For list datasets (tasks,sessions) the max is 100.cursor(optional) — an opaque value from a previous response, to fetch the next page (see Pagination).
5. Response format
{
"data": [],
"pagination": {
"limit": 100,
"nextCursor": "eyJ2Ijoy...",
"hasMore": true
},
"meta": {
"type": "sessions",
"dateRange": { "from": "...", "to": "..." },
"windowPolicy": "client-range"
}
}
data— an array of rows for list and detail datasets, or a single object for aggregate datasets.pagination—nextCursorandhasMoredrive paging. Aggregate datasets always returnnextCursor: nullandhasMore: false.meta.dateRange— the window that was actually applied. For most datasets this echoes your request; some use a fixed current-period window instead (see Datasets).- Empty results return
200with an empty array (ornullfor an aggregate) — never someone else's data.
Responses contain names, emails, and repository URLs — never internal database ids. A user is returned as a name and email, a project as its name, and a repository as its display name and clone URL. Internal id fields are removed. On an analytics tenant, sessions carry no project or repository at all, so those fields are simply absent.
6. Datasets
The type field selects a dataset. Each dataset is one of three shapes:
- Aggregate — a single object of totals or a chart series. Never paginated.
- List — a page of rows, paged by page number (max 100 per page).
- Detail — a full result set, returned in pages of up to
limitrows.
POST /beezi
Standard tenants only. On an analytics tenant this endpoint returns 404 for every dataset below — see Tenant types.
| type | shape | window | filters |
|---|---|---|---|
overview | aggregate | request range | userEmail, projectName |
cost_chart | aggregate | request range | userEmail, projectName |
task_chart | aggregate | request range | userEmail, projectName |
tasks | list | request range | userEmail, projectName |
budget_overview | aggregate | current period | projectName |
budget_forecast | aggregate | current period | projectName |
cost_attribution | aggregate | current period | projectName |
cost_attribution_details | detail | current period | projectName |
cache_optimization | aggregate | current period | projectName |
connection_attribution | aggregate | current period | projectName |
connection_attribution_details | detail | current period | projectName |
cost_optimization | aggregate | current period | projectName |
cost_optimization_details | detail | current period | projectName |
cost_by_task_status | aggregate | current period | projectName |
POST /claude-code
Available on both tenant types. The filters columns list what each dataset accepts; which of them apply depends on the tenant type.
| type | shape | window | filters (standard) | filters (analytics) |
|---|---|---|---|---|
overview | aggregate | request range | userEmail, projectName, repository | userEmail, groupName |
sessions | list | request range | userEmail, projectName, repository | userEmail, groupName |
user_leaderboard | detail | request range | userEmail, projectName, repository | userEmail, groupName |
billing_plan_details | detail | request range | userEmail, projectName, repository | userEmail, groupName |
usage_heatmap | aggregate | request range | userEmail, projectName, repository | userEmail, groupName |
Datasets marked current period (budget, forecast, attribution, cache, optimization, cost-by-task-status) always report the current billing period or forecast horizon. Your dateRange is still required and validated, but it is not applied — meta.dateRange tells you the window actually used.
7. Filters
All filters are optional.
| Filter | Matches | Available on |
|---|---|---|
userEmail | a user's primary email or any additional email — case-insensitive "contains" | both tenant types |
projectName | a project's name or its board integration URL — case-insensitive "contains" (duplicate names are all included) | standard tenants |
repository | a repository's display name or its clone URL — case-insensitive "contains" | standard tenants |
groupName | one user group, by exact name (case-insensitive) | analytics tenants |
- Only the filters listed for a dataset are accepted; sending any other filter returns
400. - A supported filter that matches nothing returns an empty result — you never accidentally receive unfiltered data.
groupName is exact, not "contains"
Unlike the other filters, groupName selects exactly one group, so it fails loudly rather than guessing:
- No group by that name, or a group you are not allowed to read —
404. These are deliberately the same answer, so a supervisor cannot discover which groups exist outside their scope. - More than one group shares that name —
400. Rename one to disambiguate.
Filters that do not apply to your tenant type are ignored rather than rejected, so one request body works against both. Sending projectName to an analytics tenant does not filter anything and does not error — check the row counts if you expected narrowing.
8. Pagination
List and detail datasets return pagination.nextCursor. To fetch the next page, resend the same request body with cursor set to that value, and repeat until hasMore is false.
# page 1
curl -sS -X POST https://<your-host>/api/v1/analytics-export/claude-code \
-H "Authorization: Bearer $ANALYTICS_KEY" -H "Content-Type: application/json" \
-d '{"type":"sessions","dateRange":{"from":"2026-01-01T00:00:00Z","to":"2026-02-01T00:00:00Z"},"limit":100}'
# page 2 — reuse nextCursor from page 1 (same body, plus cursor)
curl -sS -X POST https://<your-host>/api/v1/analytics-export/claude-code \
-H "Authorization: Bearer $ANALYTICS_KEY" -H "Content-Type: application/json" \
-d '{"type":"sessions","dateRange":{"from":"2026-01-01T00:00:00Z","to":"2026-02-01T00:00:00Z"},"limit":100,"cursor":"<nextCursor>"}'
A cursor is tied to the request that produced it — its type, dateRange, filters, and limit. Keep the whole body identical across a traversal and change only cursor. Reusing a cursor from a different request, changing limit mid-traversal, or a tampered cursor returns 400.
Cursors are short-lived and are not guaranteed to survive a Beezi release. Treat a 400 mid-traversal as "start this export again from page 1" rather than a bug in your integration — do not persist a cursor between runs.
9. Errors
Every error is JSON with an HTTP status and a message.
| Status | When |
|---|---|
| 400 | Unknown type; an invalid or mismatched cursor; an invalid date range (bad format, from not before to, or over 366 days); limit over the max; a filter the dataset does not support; an ambiguous groupName. |
| 401 | Missing, malformed, invalid, revoked, or expired API key. |
| 403 | Valid key but missing a claim or the scope; a plan without analytics; or a key whose creator was deactivated or lost their seat. |
| 404 | POST /beezi on an analytics tenant; a groupName that does not exist or that you are not allowed to read. |
| 429 | Reserved for a future per-key rate limit; not enforced today. |
| 500 | Unexpected server error. |
curl -sS -X POST https://<your-host>/api/v1/analytics-export/beezi \
-H "Authorization: Bearer $ANALYTICS_KEY" -H "Content-Type: application/json" \
-d '{"type":"nope","dateRange":{"from":"2026-01-01T00:00:00Z","to":"2026-02-01T00:00:00Z"}}'
# standard tenant → 400 {"message":"Unknown type \"nope\". Valid types: overview, cost_chart, ..."}
# analytics tenant → 404 {"message":"Cannot POST /api/v1/analytics-export/beezi"}
On an analytics tenant the 404 comes first, before the dataset name is even checked — so a bad type on /beezi reports the missing endpoint rather than the unknown dataset.
10. Rate limits & auditing
- Rate limits — not enforced today. Be a good citizen: page sequentially and cache results rather than re-pulling the same window repeatedly.
- Auditing — every successful export is recorded (the key's non-secret prefix, the endpoint and dataset, the row count, and whether the request was group-scoped or narrowed by the caller's role), and each request updates the key's last-used time, which the Manage dialog shows. The group name itself is not recorded.
11. Versioning
Endpoints are versioned in the path (/v1/). Dataset names and response shapes are stable within a version; any breaking change ships under a new version.