---
title: "Get per-project usage"
method: GET
path: "/v1/projects/{project_id}/usage"
tags: ["Projects"]
---

# Get per-project usage

`GET /v1/projects/{project_id}/usage`

## Request

- Base URL: `https://api.naturali.ai`
- URL: `https://api.naturali.ai/v1/projects/{project_id}/usage`
- Auth: one of:
  - HTTP bearer
  - OAuth 2 (authorization code, authorization URL `https://api.naturali.ai/authorize`, token URL `https://api.naturali.ai/token`, scopes: `mcp:access`)

The per-project meter — the re-billing view (A11/C12/P3). Aggregates the project's usage over an optional [from, to] window, bucketed by a single dimension. Costs are the billing-grade cost_usd the runtime freezes at write time; null means nothing in the bucket was priced (never that it was free). Only managed providers are priced (on the runtime), so cost reflects managed usage; BYOK usage carries no LLM cost.

Every bucket also carries components — the amounts actually measured. The token counts describe LLM usage alone, so that is where a storage, api_request or compute_execution bucket reports its real quantity instead of zeroed token fields.

The rollup can also be narrowed to one session or one end user before it is bucketed. That is the question no dimension answers: group_by splits the project's whole spend, so "what did this conversation cost, per day" and "what has this end user spent across every session" are reachable only by narrowing. The narrowings intersect, apply to the totals as well as the buckets, and are echoed back complete — null for each one not applied — because a rollup of zeros is otherwise indistinguishable from a project that spent nothing.

## Query parameters

- `group_by` 'model' | 'ai_provider' | 'agent' | 'run' | 'day' | 'meter_type' | 'actor' | 'session' | 'source'
- `meter_type` string
- `session_id` string
- `actor_id` string
- `agent_id` string
- `ai_provider_id` string
- `orchestration_run_id` string
- `orchestration_id` string
- `generation_id` string
- `trace_id` string
- `source` string
- `trigger_id` string
- `action_id` string
- `include` 'distinct'
- `from` string, date-time
- `to` string, date-time
- `limit` integer
- `offset` integer

## Response `200`

Usage for the project.

- ProjectUsage: The measured amounts in a bucket, one entry per component.
  - `project_id` string, required
  - `window` object, required: The [from, to] bounds applied, echoed back (null = unbounded).
    - `from` string, date-time, nullable, required
    - `to` string, date-time, nullable, required
  - `group_by` 'model' | 'ai_provider' | 'agent' | 'run' | 'day' | 'meter_type', required
  - `meter_type` string, nullable, required: The meter-type filter applied, echoed back; null when unfiltered.
  - `session_id` string, nullable, required: The session the rollup was narrowed to, echoed back; null when unnarrowed. Always present, so a caller can tell an unnarrowed rollup from one this meter did not narrow.
  - `actor_id` string, nullable, required: The actor the rollup was narrowed to, echoed back; null when unnarrowed.
  - `filters` ProjectUsageFilters, required: Every narrowing this meter knows, echoed back exactly as the caller sent it and null when unset. Echoed because a rollup of zeros is otherwise indistinguishable from a project that spent nothing, and always complete so a caller reading one key can tell "not narrowed" from "this meter does not know that narrowing". Grouped rather than spread across the top level: at twelve they would outnumber the figures, and a top-level ai_provider_id would sit beside a per-bucket ai_provider_id that means something else. The eight naming a resource (session_id, actor_id, agent_id, ai_provider_id, orchestration_run_id, orchestration_id, generation_id, trace_id) are resolved against the project and empty the rollup when they name nothing in it; the rest are matched against the value the event recorded. meter_type, session_id and actor_id also appear at the top level, where they were the response's only narrowing echo before the others existed.
    - `meter_type` string, nullable, required
    - `session_id` string, nullable, required
    - `actor_id` string, nullable, required
    - `agent_id` string, nullable, required
    - `ai_provider_id` string, nullable, required
    - `orchestration_run_id` string, nullable, required
    - `orchestration_id` string, nullable, required
    - `generation_id` string, nullable, required
    - `trace_id` string, nullable, required
    - `source` string, nullable, required
    - `trigger_id` string, nullable, required
    - `action_id` string, nullable, required
  - `distinct` ProjectUsageDistinct, nullable, required: Present only when include=distinct was sent; null otherwise — never zeroes for a rollup that did not compute it, because a counter nobody asked for must not read as "none". How many distinct entities of each kind the window touched — the counters a "how many generations / runs / end users this cycle" question reads, and what groups.total is not: bucket cardinality counts buckets, and every dimension keeps a null bucket for the events it does not apply to. Nulls are not counted here: work with no end user behind it contributes to event_count and to neither actors nor sessions, and a standalone generation counts under generations and not under orchestration_runs. None of these add up. Two adjacent windows' sessions overlap wherever a session spans the boundary, and a run that straddles midnight is in both days. A wider figure is a wider query, never a sum of narrower ones.
    - `generations` integer
    - `traces` integer
    - `orchestration_runs` integer
    - `agents` integer
    - `actors` integer
    - `sessions` integer
    - `ai_providers` integer
  - `event_count` integer, required: Metered events in the whole window, however many buckets were paged through.
  - `cost_usd` number, nullable, required: Total billing-grade cost in USD; null when nothing was priced.
  - `groups` ProjectUsageGroups, required: One page of buckets, one entry per distinct value in the chosen dimension. The rollup grows with the window — day buckets per day, model per model served — so the buckets are paged while total counts the whole window. Ordered by cost_usd descending, ties broken by key then ai_provider_id ascending, nulls last: the first page is the biggest spenders, and paging never repeats or skips a bucket.
    - `data` UsageGroup[], required
      - `key` string, nullable, required: The bucket's value in the chosen dimension (model, meter type, source, AI provider/agent/run/session/actor id, or YYYY-MM-DD day); null when it does not apply. Under `group_by=model` this is the model name, the same string `GET /v1/models` lists and an agent is configured with; under `group_by=ai_provider` it is the provider id.
      - `ai_provider_id` string, nullable, required: The AI provider that served the bucket's model, under `group_by=model`; null on every other dimension, including `group_by=ai_provider`, where the provider is `key`. The model dimension buckets on the model *and* its provider, so one model served by two providers is two groups — this is what tells them apart. The groups still sum to the totals.
      - `cost_usd` number, nullable, required: Billing-grade cost in USD; null when nothing in the bucket was priced.
      - `event_count` integer, required: Metered events in the bucket. Cost alone does not say whether a bucket is one expensive call or a thousand cheap ones, and it is not a count of generations: one generation writes an event per dimension it meters.
      - `input_tokens` integer, required
      - `output_tokens` integer, required
      - `cached_tokens` integer, required
      - `total_tokens` integer, required: input_tokens + output_tokens.
      - `components` UsageComponent[], required: Sorted by component, then unit. Empty when nothing was measured in the bucket.
        - `component` string, required: The measured dimension — input_tokens, cached_tokens, output_tokens, reasoning_tokens, compute_second, request, gb_day, … Components are not always disjoint. `reasoning_tokens` is the part of `output_tokens` a reasoning model spent thinking: it is reported for visibility and priced as output, so its own `cost_usd` is null to keep it from being counted twice. On a model that reasons by default it can be most of the output — and most of the bill — for a short answer.
        - `unit` string, required: The unit quantity is measured in.
        - `quantity` number, required: The summed measured amount, in unit. Fractional for measures like GB-days and compute seconds.
        - `cost_usd` number, nullable, required: Billing-grade cost in USD for this component; null when it was not priced, or when another component already prices it (as `output_tokens` prices `reasoning_tokens`). The quantity is still measured — null never means free.
    - `total` integer, required: Distinct buckets in the window, independent of limit, including a null bucket for the events the chosen dimension does not apply to. It is a count of buckets and not of anything they describe: under group_by=run a project that runs no orchestration has exactly one bucket whatever its volume. How many runs an account made is GET /v1/users/me/usage.
    - `limit` integer, required
    - `offset` integer, required
  - `input_tokens` integer, required
  - `output_tokens` integer, required
  - `cached_tokens` integer, required
  - `total_tokens` integer, required: input_tokens + output_tokens.
  - `components` UsageComponent[], required: Sorted by component, then unit. Empty when nothing was measured in the bucket.
    - `component` string, required: The measured dimension — input_tokens, cached_tokens, output_tokens, reasoning_tokens, compute_second, request, gb_day, … Components are not always disjoint. `reasoning_tokens` is the part of `output_tokens` a reasoning model spent thinking: it is reported for visibility and priced as output, so its own `cost_usd` is null to keep it from being counted twice. On a model that reasons by default it can be most of the output — and most of the bill — for a short answer.
    - `unit` string, required: The unit quantity is measured in.
    - `quantity` number, required: The summed measured amount, in unit. Fractional for measures like GB-days and compute seconds.
    - `cost_usd` number, nullable, required: Billing-grade cost in USD for this component; null when it was not priced, or when another component already prices it (as `output_tokens` prices `reasoning_tokens`). The quantity is still measured — null never means free.

## Other responses

- `401`: Missing or invalid credentials.
- `404`: The resource does not exist (existence is not leaked).

---

[API](https://skmtc.dev/naturali/apis/naturali-ai-api.md) · [All operations](https://skmtc.dev/naturali/apis/naturali-ai-api/llms.txt) · [OpenAPI document](https://skmtc.dev/naturali/apis/naturali-ai-api/revisions/e4905b78f422?raw)
