Skip to main content

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 tenantsTenant Owner and Admin.
  • Analytics tenantsTenant 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

  1. In the Manage API keys dialog, click Add key.
  2. Optionally enter a label (default Analytics API Key) to recognize the key later.
  3. Click Create key.
  4. Copy the secret shown on the next screen.
IMPORTANT

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

IMPORTANT

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

StatusMeaning
ActiveLive and usable. Counts toward your 5-key limit.
PendingA slot reserved while the key is being created (transient — you rarely see it).
RevokedTurned 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.

NOTE

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 tenantAnalytics tenant
POST /beeziavailable404 — cost analytics do not exist here
POST /claude-codeavailableavailable
Organized byprojects and repositoriesuser groups
FiltersuserEmail, projectName, repositoryuserEmail, groupName
Data you receivethe whole tenantscoped 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 roleReads
Tenant Owner, Adminevery member of the tenant
Supervisorevery 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.

IMPORTANT

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.

IMPORTANT

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/beezi
  • POST 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. from is inclusive, to is exclusive, from must be before to, 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.
  • paginationnextCursor and hasMore drive paging. Aggregate datasets always return nextCursor: null and hasMore: 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 200 with an empty array (or null for an aggregate) — never someone else's data.
NOTE

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 limit rows.

POST /beezi

NOTE

Standard tenants only. On an analytics tenant this endpoint returns 404 for every dataset below — see Tenant types.

typeshapewindowfilters
overviewaggregaterequest rangeuserEmail, projectName
cost_chartaggregaterequest rangeuserEmail, projectName
task_chartaggregaterequest rangeuserEmail, projectName
taskslistrequest rangeuserEmail, projectName
budget_overviewaggregatecurrent periodprojectName
budget_forecastaggregatecurrent periodprojectName
cost_attributionaggregatecurrent periodprojectName
cost_attribution_detailsdetailcurrent periodprojectName
cache_optimizationaggregatecurrent periodprojectName
connection_attributionaggregatecurrent periodprojectName
connection_attribution_detailsdetailcurrent periodprojectName
cost_optimizationaggregatecurrent periodprojectName
cost_optimization_detailsdetailcurrent periodprojectName
cost_by_task_statusaggregatecurrent periodprojectName

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.

typeshapewindowfilters (standard)filters (analytics)
overviewaggregaterequest rangeuserEmail, projectName, repositoryuserEmail, groupName
sessionslistrequest rangeuserEmail, projectName, repositoryuserEmail, groupName
user_leaderboarddetailrequest rangeuserEmail, projectName, repositoryuserEmail, groupName
billing_plan_detailsdetailrequest rangeuserEmail, projectName, repositoryuserEmail, groupName
usage_heatmapaggregaterequest rangeuserEmail, projectName, repositoryuserEmail, groupName
NOTE

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.

FilterMatchesAvailable on
userEmaila user's primary email or any additional email — case-insensitive "contains"both tenant types
projectNamea project's name or its board integration URL — case-insensitive "contains" (duplicate names are all included)standard tenants
repositorya repository's display name or its clone URL — case-insensitive "contains"standard tenants
groupNameone 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.
NOTE

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.

NOTE

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.

StatusWhen
400Unknown 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.
401Missing, malformed, invalid, revoked, or expired API key.
403Valid key but missing a claim or the scope; a plan without analytics; or a key whose creator was deactivated or lost their seat.
404POST /beezi on an analytics tenant; a groupName that does not exist or that you are not allowed to read.
429Reserved for a future per-key rate limit; not enforced today.
500Unexpected 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"}
NOTE

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.