--- title: "Observe" description: "Read-only usage summaries and audit events for a workspace, windowed in UTC and paged on opaque cursors." --- Two read-only projections over control's own tables. Both take a closed UTC window, both page on an opaque cursor, and both validate their response against a published schema before returning it — a body that does not match the contract is a `503`, not a partial answer. ## Authentication `Authorization: Bearer `, verified as [/api-reference/overview](/api-reference/overview) describes. Usage needs the `usage.read` action and audit needs `audit.read`; both are granted to every role including viewer. A non-member gets `404 workspace not found`. Query errors answer `422` with a field-shaped detail: ```json { "detail": [{ "loc": ["query", "from"], "msg": "invalid UTC window", "type": "value_error" }] } ``` A storage or validation failure answers `503 Observe read model unavailable`. ## Usage summary `GET /api/v1/workspaces/{workspace_id}/usage` Aggregate usage over a window, grouped one way. Start of the UTC window, 1–64 characters. Inclusive. End of the UTC window, 1–64 characters. Exclusive. One of `kind`, `key`, `model`, `provider`, `tool`. 1–200 groups. Opaque group cursor from a previous `next_cursor`, at most 2048 characters. Bound to the same `group_by`. ### Response `{from, to}`, echoed as parsed. Aggregates over the whole window: `event_count`, `llm_requests`, `unpriced_count`, `usd_micros`, `tokens_in`, `tokens_out`, `tokens_cached`, `tokens_reasoning`, `tool_calls`, `partial_count`, `failure_count`. Every value is an exact integer — sums cross the JSON boundary without a float conversion. Sums only events whose `pricing_status` is `known`. Events priced `unknown` are counted in `unpriced_count` and contribute nothing to spend, so an unpriced call is never quietly billed as free. `{value, totals}` per group, where `value` is the grouped column and may be `null`. `null` sorts last. Per-key budget posture for the current calendar month, read from the gateway's spend counters. The most recent ingestion timestamp inside the window, or `null` when the window is empty. `complete`, or `gap_detected` when the workspace has a recorded usage ingestion gap. A summary that might be missing events says so rather than looking whole. Cursor for the next page of groups, or `null`. ```bash Request curl "https://control.tryobol.dev/api/v1/workspaces/ws_acme_prod/usage?from=2026-09-01T00:00:00Z&to=2026-09-04T00:00:00Z&group_by=tool&limit=50" \ -H "Authorization: Bearer $OBOL_SESSION" ``` ```json Response { "schema_version": 1, "workspace_id": "ws_acme_prod", "window": { "from": "2026-09-01T00:00:00Z", "to": "2026-09-04T00:00:00Z" }, "group_by": "tool", "totals": { "event_count": 412, "llm_requests": 271, "unpriced_count": 3, "usd_micros": 184220, "tokens_in": 903112, "tokens_out": 118440, "tokens_cached": 22100, "tokens_reasoning": 0, "tool_calls": 141, "partial_count": 0, "failure_count": 6 }, "groups": [ { "value": "github.create_issue", "totals": { "event_count": 88, "llm_requests": 0, "unpriced_count": 0, "usd_micros": 0, "tokens_in": 0, "tokens_out": 0, "tokens_cached": 0, "tokens_reasoning": 0, "tool_calls": 88, "partial_count": 0, "failure_count": 1 } } ], "budgets": [], "freshness_at": "2026-09-03T23:58:11Z", "completeness": "complete", "next_cursor": null } ``` {/* TODO: the `budgets` element shape is produced by `usage_query.read_budget_posture`; document its fields once the operator budget contract is settled. */} See [/billing/usage](/billing/usage) for how these events are metered and [/billing/metering](/billing/metering) for what is counted. ## Audit events `GET /api/v1/workspaces/{workspace_id}/audit` Operator actions in the workspace, newest first. Start of the UTC window, 1–64 characters. End of the UTC window, 1–64 characters. Filter to one action, at most 64 characters. Must be in the closed `OperatorAuditAction` vocabulary — an unknown value is `422 unknown audit action` rather than an empty list. Filter to one operator, at most 64 characters. Filter to one target id, at most 128 characters. 1–200. Opaque time cursor from a previous `next_cursor`, at most 2048 characters. ### Actions ```text workspace.create key.mint key.revoke catalog.provider.register webhook.configure webhook.delete connection.start connection.credential.store connection.federated.complete connection.reconnect connection.disable connection.credential.purge connection.discard model_connection.start model_connection.credential.store model_connection.disable policy.draft.store policy.publish route_qualifier.upsert route_qualifier.delete approval.approve approval.deny approval_policy.create approval_policy.revise approval_policy.disable pricing.replace ``` Rows whose stored action is outside this vocabulary are excluded from the listing entirely. ### Response `{user_id, email}`. The email is dropped to `null` if it exceeds 320 characters. The acted-on id, or `null` when it exceeds 256 characters. A filtered subset of the stored detail. Only these keys survive: `agent_id`, `bundle_hash`, `connection_id`, `env`, `key_id`, `last4`, `policy_id`, `prefix`, `provider_id`, `publication_state`, `qualifier_id`, `reason`, `revision_id`, `snapshot_version`, `source`, `status`, `tier`. At most 16 keys, and each value must be a string of at most 256 characters, a boolean, an `int64`, or a list of at most 16 such strings. Everything else is dropped, so an audit detail can never carry an unreviewed value out. Cursor for the next page, or `null`. ```bash Request curl "https://control.tryobol.dev/api/v1/workspaces/ws_acme_prod/audit?from=2026-09-01T00:00:00Z&to=2026-09-05T00:00:00Z&action=connection.credential.store" \ -H "Authorization: Bearer $OBOL_SESSION" ``` ```json Response { "schema_version": 1, "workspace_id": "ws_acme_prod", "window": { "from": "2026-09-01T00:00:00Z", "to": "2026-09-05T00:00:00Z" }, "events": [ { "schema_version": 1, "event_id": "aud_01J8Z1", "workspace_id": "ws_acme_prod", "actor": { "user_id": "usr_7c1", "email": "ops@acme.test" }, "action": "connection.credential.store", "target": "conn_9f2c1d", "detail": { "last4": "cret", "status": "active" }, "occurred_at": "2026-09-03T14:02:40Z" } ], "next_cursor": null } ``` See [/observability/audit-log](/observability/audit-log) for what each action records.