> ## Documentation Index
> Fetch the complete documentation index at: https://docs.tess.im/llms.txt
> Use this file to discover all available pages before exploring further.

# Mission Control usage

> Query the AI consumption of a workspace (credits, executions, adoption) from Mission Control: one period, grouped, filtered and compared with the previous period.

Your API token must be allowed to **use agents** (`use_agents`) and you need API access in the workspace. On top of that, the endpoint applies the **Mission Control** permissions of your role: `mission-control:view` to query at all, and the usage scope you were granted (your own usage, your teams, or the whole workspace). The scope is resolved on the server from your role; the request never widens it.

> [!NOTE]
>
> This endpoint answers exactly what the Mission Control panel in Tess shows, from the same hourly data. Mission Control must be enabled for the workspace, and data exists from `freshness.data_available_from` onwards. A period that starts earlier is refused, never answered with zeros.

### **Code examples**

Consumption by AI model in the current month, compared with the previous period:

```http cURL theme={null}
curl --request GET \
  --url 'https://api.tess.im/mission-control/usage?from=2026-10-01&to=2026-10-28&timezone=America/Sao_Paulo&group_by[]=resource&filters[resource_kind][]=model&compare=previous_period&granularity=month&limit=5' \
  --header 'Authorization: Bearer YOUR_API_KEY' \
  --header 'x-workspace-id: YOUR_WORKSPACE_ID' \
  --header 'Accept: application/json'
```

```javascript Node.js theme={null}
const axios = require('axios');

const config = {
  method: 'get',
  url: 'https://api.tess.im/mission-control/usage',
  params: {
    from: '2026-10-01',
    to: '2026-10-28',
    timezone: 'America/Sao_Paulo',
    'group_by[]': ['resource'],
    'filters[resource_kind][]': ['model'],
    compare: 'previous_period',
    granularity: 'month',
    limit: 5
  },
  headers: {
    'Authorization': 'Bearer YOUR_API_KEY',
    'x-workspace-id': 'YOUR_WORKSPACE_ID',
    'Accept': 'application/json'
  }
};

try {
  const response = await axios(config);
  console.log(response.data.data.totals, response.data.data.groups);
} catch (error) {
  console.error(error.response?.status, error.response?.data);
}
```

```python Python theme={null}
import requests

url = "https://api.tess.im/mission-control/usage"
params = {
    "from": "2026-10-01",
    "to": "2026-10-28",
    "timezone": "America/Sao_Paulo",
    "group_by[]": ["resource"],
    "filters[resource_kind][]": ["model"],
    "compare": "previous_period",
    "granularity": "month",
    "limit": 5,
}
headers = {
    "Authorization": "Bearer YOUR_API_KEY",
    "x-workspace-id": "YOUR_WORKSPACE_ID",
    "Accept": "application/json",
}

response = requests.get(url, params=params, headers=headers)
if response.ok:
    data = response.json()["data"]
    print(data["totals"], data["groups"])
else:
    print(response.status_code, response.json())
```

### **Headers**

**x-workspace-id** `integer` *required*

Workspace ID. You must be an active member of the workspace and hold `mission-control:view` on it.

### **Query parameters**

**from** `date (YYYY-MM-DD)` *required*

Start of the period, a local date in `timezone` (inclusive).

**to** `date (YYYY-MM-DD)` *required*

End of the period (inclusive). The period is at most **93 days** and cannot start before `freshness.data_available_from`.

**timezone** `string`

IANA timezone of the local dates and of the buckets (for example `America/Sao_Paulo`). Default: the platform timezone.

**granularity** `string`

Bucket size of `series`: `hour`, `day` (default), `week` or `month`.

**metrics\[]** `array of string`

Metrics to return. Default: `consumption_net`, `consumption_charged`, `consumption_refunded`. Available: `consumption_net`, `consumption_charged`, `consumption_refunded`, `executions`, `failed_executions`, `interrupted_executions`, `uses`, `success_rate`, `avg_credits_per_execution`, `avg_credits_per_person`, `active_people`, `active_agents`, `active_autonomous_agents`, `active_days`, `active_business_days`.

**group\_by\[]** `array of string`

Dimensions to group by, at most 3: `agent`, `person`, `resource`, `resource_kind`, `execution_type`, `modality`, `activity_category`, `charge_type`, `execution_origin`, `trigger_source`, `agent_origin`. Grouping by `person` needs `mission-control:person-data:view`; without it the query is refused (403 `person_data_not_allowed`).

**filters\[person]\[]** `array of string`

`me` (your own usage), `my_team` (current members of your teams), `my_workspace` (everyone in your scope) or person ids. The aliases need no extra permission; person ids need `mission-control:person-data:view` and must be inside your scope.

**filters\[team]\[]** `array of integer`

Team ids. Only the ids listed in `scope.teams` of a previous answer are accepted.

**filters\[agent]\[]** `array of integer`

Agent ids. Take them from `groups[].dimensions.agent.id` of a previous answer grouped by `agent`.

**filters\[resource]\[]** `array of string`

Resources as `<kind>:<name>`, for example `model:gpt-5`, `tool:web_search`, `connector:googlesheets`. Take them from a previous answer grouped by `resource`.

**filters\[resource\_kind]\[]** `array of string`

`model`, `tool`, `connector`, `upload`, `kb`, `memory`, `voice` or `other`. Use `["model"]` to restrict a `resource` grouping to AI models.

**filters\[modality]\[]** `array of string`

`chat`, `image`, `video`, `audio`, `upload`, `tools` or `other`.

**filters\[activity\_category]\[]** `array of string`

`agents`, `tools`, `connectors` or `kb`.

**filters\[agent\_origin]\[]** `array of string`

`standard` or `cowork` (Cowork / Digital Employee agents).

**filters\[trigger\_source]\[]** `array of string`

Who started the execution: `human`, `scheduler`, `autopilot`, `automation`, `api` or `embed`.

**filters\[status]\[]** `array of string`

Run outcome: `succeeded`, `failed` or `interrupted` (stopped by the person, still billed).

**filters\[execution\_type]\[]** `array of string`

Exact execution type values, as returned in a previous grouped answer (for example `chat`, `text`, `image`, `tool_execution`).

**filters\[charge\_type]\[]** `array of string`

Exact charge type values (for example `execution`).

**filters\[execution\_origin]\[]** `array of string`

Exact execution origin values.

**compare** `string`

`previous_period` (same number of days, right before the period) or `previous_calendar_month` (the whole previous calendar months, at most 3). Adds `comparison` to the answer and to every group.

**sort** `string`

Metric id the groups are ordered by. Default: the first requested metric.

**sort\_by** `string`

`value` (default), `delta_abs` or `delta_pct` (change versus the comparison period; both need `compare`).

**order** `string`

`desc` (default) or `asc`. Null values always come last.

**limit** `integer`

Groups per page. Default 50, maximum 200.

**cursor** `string`

Opaque cursor of the next page (`page.next_cursor` of a previous answer).

**include\_group\_series** `boolean`

Also return the series of every group.

**Array parameters**

Send repeated keys with brackets, exactly as shown: `group_by[]=resource&filters[person][]=me&filters[person][]=my_team`. Filters only narrow the data; a value outside your scope is refused, never silently dropped.

**IDs: never guess them**

* Team ids come only from `scope.teams` of an answer (`scope.teams_total` is the uncapped count).
* Agent, person and resource ids come from `groups[].dimensions.<dimension>.id` of a previous answer grouped by that dimension.
* Closed sets (`modality`, `activity_category`, `agent_origin`, `trigger_source`, `resource_kind`, `status`) are listed above; open values (`execution_type`, `charge_type`, `execution_origin`) come from a previous grouped answer.

**Resources and non-additive totals**

Grouping or filtering by `resource`, `resource_kind` or `charge_type` counts the executions that **used** each resource. A run that used two tools counts once in each group, so across resources the totals of the run metrics (`executions`, `failed_executions`, `success_rate`, `avg_credits_per_execution`) come back as `null` and `meta.non_additive` tells which ones: read the per-group values, never add the groups up. Credits and `uses` always add up. Model-only scopes (`filters[resource_kind][]=model`) stay exact.

### **Response**

```json theme={null}
{
  "data": {
    "workspace_id": 4645,
    "scope": {
      "level": "workspace",
      "can_view_person_data": true,
      "can_view_cost": true,
      "downgrade_reason": null,
      "teams": [],
      "teams_total": 0
    },
    "period": {
      "from": "2026-10-01",
      "to": "2026-10-28",
      "timezone": "America/Sao_Paulo",
      "days": 28,
      "granularity": "month",
      "starts_at": "2026-10-01T03:00:00Z",
      "ends_at": "2026-10-29T03:00:00Z"
    },
    "metrics": {
      "consumption_net": {
        "id": "consumption_net",
        "name": "Net consumption",
        "unit": "credits",
        "aggregation": "sum",
        "max_window_days": 93
      }
    },
    "totals": {
      "consumption_net": 22.44224,
      "consumption_charged": 22.44224,
      "consumption_refunded": 0
    },
    "series_metrics": ["consumption_net", "consumption_charged", "consumption_refunded"],
    "series": [
      {
        "bucket": "2026-10-01",
        "starts_at": "2026-10-01T03:00:00Z",
        "ends_at": "2026-10-29T03:00:00Z",
        "complete": false,
        "values": {
          "consumption_net": 22.44224,
          "consumption_charged": 22.44224,
          "consumption_refunded": 0
        }
      }
    ],
    "dimensions": {
      "group_by": ["resource"],
      "filters": { "resource_kind": ["model"] },
      "resolved_filters": {}
    },
    "groups": [
      {
        "key": "[\"model:claude-fable-5.1\"]",
        "dimensions": {
          "resource": {
            "id": "model:claude-fable-5.1",
            "kind": "model",
            "name": "claude-fable-5.1",
            "label": "claude-fable-5.1",
            "kind_label": "AI model"
          }
        },
        "totals": {
          "consumption_net": 19.908,
          "consumption_charged": 19.908,
          "consumption_refunded": 0
        },
        "share": {
          "consumption_net": 0.887077,
          "consumption_charged": 0.887077,
          "consumption_refunded": null
        }
      }
    ],
    "page": {
      "limit": 5,
      "sort": "consumption_net",
      "sort_by": "value",
      "order": "desc",
      "total_groups": 6,
      "has_more": true,
      "next_cursor": "eyJmIjoi..."
    },
    "freshness": {
      "data_available_from": "2026-09-28T00:00:00Z",
      "data_as_of": "2026-10-28T19:50:00Z",
      "partial": false
    },
    "comparison": {
      "type": "previous_period",
      "period": { "from": "2026-09-03", "to": "2026-09-30", "days": 28 },
      "available": false,
      "reason": "period_before_coverage"
    },
    "meta": {
      "catalog_version": 6,
      "resource_scoped": true,
      "non_additive": {},
      "notes": [],
      "limits": {
        "max_window_days": 93,
        "max_group_by": 3,
        "max_filter_values": 100,
        "default_page_size": 50,
        "max_page_size": 200
      }
    }
  }
}
```

### **Response fields**

| **Field** | **Description** |
| :- | :- |
| scope | What you may read: `level` (`own`, `team` or `workspace`), `can_view_person_data`, `can_view_cost`, `teams` (the ids `filters[team][]` accepts, with `mine` marking your own teams) and `teams_total`. `downgrade_reason` says why the level is narrower than your role asked for (`null` otherwise). |
| period | The requested period as resolved: local dates, timezone, number of days, granularity and the UTC instants. |
| metrics | Definition of every returned metric: id, name, definition, unit, aggregation, maximum window and supported comparisons. Rollup internals are not part of the public contract. |
| totals | Value of each metric for the whole period. `null` when the metric is non-additive across the requested resources (see `meta.non_additive`). |
| series | One bucket per granularity step, zero-filled; `complete` is `false` for the bucket still in progress. |
| series\_metrics | The metric ids carried by every `series[].values` entry, in order. |
| dimensions | Echo of `group_by` and `filters`; `resolved_filters.person` shows how the person aliases were resolved (`aliases`, `resolved_count`, and `user_ids` only with person-data permission). |
| groups | When `group_by` is given: one entry per group with `dimensions` (always carrying the id to reuse as a filter), `totals`, `share` (part of the period total, additive metrics only) and, with `compare`, `comparison` and `change_abs`. |
| page | `limit`, `sort`, `sort_by`, `order`, `total_groups`, `has_more` and `next_cursor` for the next page. |
| freshness | `data_available_from` (first day with data), `data_as_of` (how fresh the data is) and `partial` (`true` while the period includes hours not yet processed). |
| comparison | With `compare`: the comparison period, `available`, totals, `delta` and, for `consumption_net` and `executions`, a `decomposition` into people effect and intensity effect. `reason` explains an unavailable comparison. |
| meta | `catalog_version`, `non_additive`, `notes` (for example `small_group` on a small `my_team` aggregate) and the hard `limits`. |

### **Errors**

Query refusals are JSON objects with a stable `code`. Request validation failures (an unknown `granularity`, `compare`, `sort_by` or `order`, a malformed date) come back as a 422 in Laravel's `{message, errors}` shape instead; access refusals (403/404) carry only `message`:

```json theme={null}
{
  "message": "The period is too long for the requested metrics.",
  "type": "error",
  "status": 422,
  "code": "window_too_large",
  "details": { "max_window_days": 93, "requested_days": 120 }
}
```

| **Status** | **Code** | **When** |
| :- | :- | :- |
| 401 | | Missing or invalid token. |
| 403 | | No API access in the workspace, not a member, Mission Control disabled, or no `mission-control:view`. Only `message`, no `code`; the same answer for every case. |
| 404 | | The workspace in `x-workspace-id` does not exist. |
| 403 | `person_data_not_allowed` | `group_by[]=person` or person ids without `mission-control:person-data:view`. Use `filters[person][]=me` or `my_team` instead. |
| 403 | `filter_outside_scope` | A person or team id outside your scope. |
| 422 | `window_too_large`, `period_before_coverage`, `invalid_period`, `invalid_timezone`, `unsupported_timezone`, `unknown_metric`, `metric_not_queryable`, `unsupported_dimension`, `unsupported_filter`, `invalid_filter`, `invalid_sort`, `invalid_cursor`, `no_team`, `invalid_page_size`, `too_many_dimensions`, `too_many_groups`, `row_limit_exceeded` | Invalid or too large query; `details` carries the limit or the offending value. `no_team`: `my_team` with no active team. |
| 503 | `usage_rollup_disabled`, `data_not_ready` | Usage data not available yet: for this workspace (`usage_rollup_disabled`, `details.reason`: `workspace_not_enabled` or `workspace_backfill_pending`; empty `details` (no `details.reason`) when usage data is disabled for every workspace) or at all (`data_not_ready`, the usage history has not been built). Retry later; the answer is never zeros. |


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.