Audit log, usage, spend limits and groups
Developer·12 minutes to read
What it does
The administrator's routes: read the organisation's audit log, its usage per member per day and per metered call, each member's on-demand spend this cycle, set one member's spend limit or many at once, manage the groups of people a team bot is shared with — one at a time or up to 500 people per call — and manage service accounts. Each route reads or writes the same rows as the app's own screens — Organisation → Audit, Organisation → Usage, the usage tab's spend limit and Organisation → Groups — so a change made here shows there. No route on this page returns conversation content.
Authentication
Every route on this page needs an organisation key of scope admin, created by a person who is still an owner or admin, except the group reads (GET /groups, GET /groups/{id}, GET /groups/{id}/members), which a read key reaches, and GET /enterprise/audit-events, which takes an enterprise key. A read or write key on an admin route, or a key whose creator has since lost the admin role, gets 401 unauthenticated. See Authentication and keys.
Request
GET /audit-events
<!-- generated:begin GET /audit-events --> <!-- generated:end GET /audit-events -->The audit log is an Enterprise feature: without the plan or its trial the route answers 403 forbidden with a details entry for plan. Oldest first, page by page with limit and cursor. from and to take a day (YYYY-MM-DD, the whole UTC day) or an ISO instant; to defaults to now and from to thirty days before to. A window wider than thirty days is refused with invalid_request and a details entry for to. limit takes 1 to 500 rows a page (default 50); 501 is refused with invalid_request and a details entry for limit.
actions is a comma-separated list of action names (at most 50). actors is a comma-separated list of at most 50 members, each given by user id or by email address in any letter case, mixed freely: actors=7c1e…,dana@example.com. An email address is looked up among the organisation's current members, and one that names nobody there — including an address that belongs to someone in another organisation — is refused with invalid_request and a details entry for actors.<position> (actors.1 for the second value); the address itself is never echoed back. A user id is taken as given, so the rows of someone who has since left stay reachable by the id the rows carry. Filtering by a member's email address returns exactly the rows filtering by their id returns.
q is a case-insensitive search of the action, the target, the row's details and the acting member's email address: q=dana@ finds every row Dana wrote.
surfaces is a comma-separated list of the surfaces a row was written from: web (the browser), desktop (the desktop app), phone (the phone app), api (this API, under a key) and bot (a bot acting during a run). surfaces=api,bot returns only the rows written from those two. Any other value is refused with invalid_request and a details entry for surfaces.
The filters combine: a row must match every one given.
Every row carries surface, one of the five values above — the same value the Organisation → Audit page and the audit export show for that row. A row written by a call to this API reads api. A row written before surfaces were recorded reads null, and surfaces never matches it.
GET /enterprise/audit-events
<!-- generated:begin GET /enterprise/audit-events --> <!-- generated:end GET /enterprise/audit-events -->The audit log of a whole enterprise in one call: every team organisation linked to it (Organisation → Enterprise) and the enterprise's own events — linking a team, admins, single sign-on, the SCIM token, groups and mappings, keys. This route takes an enterprise API key, created on Organisation → Enterprise with the scope audit_read or admin; an organisation key is 401 unauthenticated here, and an enterprise key is 401 on every other route. Each row is the team route's row plus organisationId: the linked team it is from, or null for an enterprise event. organisationId as a query parameter answers one linked team only; a team that is not linked to the key's enterprise is 404 not_found. The window (from, to, at most thirty days), the filters (actions, actors, q) and the paging (limit, cursor, oldest first) are those of GET /audit-events. An organisation unlinked from the enterprise leaves the feed from the next call on.
GET /usage/daily
<!-- generated:begin GET /usage/daily --> <!-- generated:end GET /usage/daily -->One row per member per UTC day between from and to, both required and inclusive. spendMicroCents and credits are what the credit ledger drew, the figure Organisation → Usage sums, so a month's rows add up to that page's totals; costMicroCents is the list-price charge. runs counts the runs that made at least one metered call that day.
GET /usage/events
<!-- generated:begin GET /usage/events --> <!-- generated:end GET /usage/events -->One row per metered model call, oldest first: the member, the bot, the run, the model route, the tokens, the credits and the cost.
GET /spend
<!-- generated:begin GET /spend --> <!-- generated:end GET /spend -->Every member's on-demand spend this cycle, their own limit (null when none is set) and the limit that binds them — the lower of theirs and the organisation's.
PUT /members/{userId}/spend-limit
<!-- generated:begin PUT /members/{userId}/spend-limit --> <!-- generated:end PUT /members/{userId}/spend-limit -->PUT /spend-limits
<!-- generated:begin PUT /spend-limits --> <!-- generated:end PUT /spend-limits -->limitMicroCents is a whole number of micro-cents (one credit is 1 000 000), 0 for a zero limit, or null to clear the member's own limit so a group limit, the team default or the organisation's cap binds instead. The limit is on the member's additional usage this calendar month — what prepaid credit pays for past the plan; on a pooled Enterprise account it is on their total usage. One bulk call sets up to 200 members, in updates (the older field limits is still read, up to 100; send exactly one of the two), and writes every limit or none. Both routes answer 250 requests a minute per key. A member at their limit has their next run refused, exactly as if the limit had been set in the app; a run already working when the limit is reached finishes its task. Hosted only: on the home edition both routes answer 404 not_found.
Groups
<!-- generated:begin GET /groups --> <!-- generated:end GET /groups --> <!-- generated:begin POST /groups --> <!-- generated:end POST /groups --> <!-- generated:begin PATCH /groups/{id} --> <!-- generated:end PATCH /groups/{id} --> <!-- generated:begin DELETE /groups/{id} --> <!-- generated:end DELETE /groups/{id} --> <!-- generated:begin GET /groups/{id}/members --> <!-- generated:end GET /groups/{id}/members --> <!-- generated:begin POST /groups/{id}/members --> <!-- generated:end POST /groups/{id}/members --> <!-- generated:begin DELETE /groups/{id}/members/{userId} --> <!-- generated:end DELETE /groups/{id}/members/{userId} -->POST /groups needs an Idempotency-Key. A deleted group is kept for the audit log and disappears from every list.
PUT /group-memberships
<!-- generated:begin PUT /group-memberships --> <!-- generated:end PUT /group-memberships -->Sets the groups of up to 500 people in one call (S-98210, hosted only). Each row is the whole set of groups that person should be in: groups it names are joined, groups it leaves out are left, and a row naming the groups the person is already in writes nothing. The answer has one result per row, in order — added and removed, or that row's own error (invalid_request for someone who is not a member, not_found for a group that is not this organisation's). A failed row changes nothing and the other rows still land; POST /batch is the all-or-nothing call. More than 500 rows, or one person twice, is refused as a whole with invalid_request. Every change is audited as org.member_group_member_added or org.member_group_member_removed. The home edition answers 405 not_supported.
Service accounts
A service account is a named, non-human account for an automated workflow — a nightly report, a sync job, a deploy hook (S-98203). It has its own API keys, its own monthly spend limit, and an archive that ends it. It is not a person: it holds no seat, signs in to nothing and is sent no mail. Its keys are organisation keys, so they reach the same routes any organisation key of their scope reaches. The same accounts are on Organisation → Service accounts in the app. A service account's runs count in the organisation's usage total and under no member, not under the owner its keys act as. Service accounts are part of the hosted edition: on the home edition these routes answer 405 not_supported, the app has no Service accounts page (the nav leaves it out and its address is a 404), and every other route works as before.
GET /service-accounts
<!-- generated:begin GET /service-accounts --> <!-- generated:end GET /service-accounts -->Every account, live first, newest first, page by page with limit and cursor. monthSpendMicroCents is what the account's runs have spent on model calls this calendar month (UTC).
POST /service-accounts
<!-- generated:begin POST /service-accounts --> <!-- generated:end POST /service-accounts -->name is 1 to 60 characters and unique among the organisation's live accounts, in any case; an archived account's name is free again. spendLimitMicroCents is optional. Needs an Idempotency-Key. Audited as org.service_account_created.
GET /service-accounts/{id}
<!-- generated:begin GET /service-accounts/{id} --> <!-- generated:end GET /service-accounts/{id} -->PUT /service-accounts/{id}/spend-limit
<!-- generated:begin PUT /service-accounts/{id}/spend-limit --> <!-- generated:end PUT /service-accounts/{id}/spend-limit -->A whole number of micro-cents sets the account's own monthly limit, 0 included; null clears it. Before each run one of the account's keys starts — POST /runs, POST /conversations/{id}/messages, or a run.create in POST /batch — the month's spend is read against the limit, and at or past it the request is refused with 429 quota_exceeded and starts nothing; its details[0].message names the limit ("This service account has reached its monthly spend limit of 500000 micro-cents."). A run already working when the limit is reached finishes its task. A batch with no run.create is not held to the limit. Audited as org.service_account_spend_limit_set, with details.from and details.to.
POST /service-accounts/{id}/archive
<!-- generated:begin POST /service-accounts/{id}/archive --> <!-- generated:end POST /service-accounts/{id}/archive -->Ends the account: every live key it holds is revoked at once, and it takes no new key or limit. Archiving an archived account answers it as it stands. Audited as org.service_account_archived, with the revoked key ids in details.revokedKeyIds.
GET /service-accounts/{id}/keys
<!-- generated:begin GET /service-accounts/{id}/keys --> <!-- generated:end GET /service-accounts/{id}/keys -->POST /service-accounts/{id}/keys
<!-- generated:begin POST /service-accounts/{id}/keys --> <!-- generated:end POST /service-accounts/{id}/keys -->The answer is the one time the key is shown. The name and expiry rules and the live-key ceiling are every key's (Authentication and keys). Needs an Idempotency-Key.
POST /service-accounts/{id}/keys/{keyId}/rotate
<!-- generated:begin POST /service-accounts/{id}/keys/{keyId}/rotate --> <!-- generated:end POST /service-accounts/{id}/keys/{keyId}/rotate -->Revokes the key and mints its replacement — same name, same scope — in one step. The new key lives as long as the old one was minted for unless expiresInDays says otherwise. The old key is refused from the next request on. Needs an Idempotency-Key. Audited as org.service_account_key_rotated, with details.oldKeyId and details.newKeyId.
On an organisation with the Enterprise plan or its trial:
curl -sS "$BOTSEON_API_URL/api/v1/audit-events?actions=org.member_group_created&limit=50" \
-H "authorization: Bearer $BOTSEON_API_KEY" \
-H "accept: application/json"
Response
{
"data": [
{
"id": "0192a3b4-c5d6-7e8f-9a0b-1c2d3e4f5a6b",
"action": "org.member_group_created",
"target": "6f1e2d3c-4b5a-4968-8776-655443322110",
"actor": { "type": "user", "userId": "8c1d4e5f-2a3b-4c5d-6e7f-8a9b0c1d2e3f" },
"botId": null,
"details": { "groupId": "6f1e2d3c-4b5a-4968-8776-655443322110", "name": "Finance" },
"surface": "web",
"createdAt": "2026-10-14T09:12:00.000Z"
}
],
"nextCursor": null
}
Errors
| Status | Code | When | What to do |
|---|---|---|---|
| 401 | unauthenticated | The key is missing, refused, below admin, or its creator is no longer an admin. | Use an admin key from a current admin. |
| 400 | invalid_request | A window over thirty days, a start after its end, a malformed list, a spend limit that is not a whole number of zero or more, more than 200 members in updates (100 in limits), or both or neither of the two, in one PUT /spend-limits, a group name out of range, or a person who is not a member added to a group. | Read details. |
| 403 | forbidden | The audit log on an organisation without the Enterprise plan or its trial; a group spend limit without Enterprise. | Upgrade, or start the trial. |
| 404 | not_found | No such member, group or service account in this organisation, the person is not in the group, or the key is not one of the service account's live keys, or a spend-limit route on the home edition. | List first. |
| 405 | not_supported | A service-account or bulk membership route on the home edition. | These are hosted features. |
| 409 | conflict | A group name, or a live service account's name, already in use; or the service account is archived. | Pick another name; create a new account. |
| 429 | quota_exceeded | A run-starting route, called with a service account's key, after its monthly limit is reached. | Raise or clear the limit, or wait for the next month. |
| 429 | rate_limited | A bucket is full. | Wait for Retry-After. |
Rate limits
The same one-minute buckets as every other route: 300 requests per key and 1 000 per organisation, with failed authentication counted against the caller's address. PUT /members/{userId}/spend-limit and PUT /spend-limits share one more: 250 requests a minute per key. See Envelopes, errors, idempotency and paging.
Last verified against build 76e3778.