Skip to main content
GET
Usage summary statistics
All /api/api-usage/* endpoints aggregate non-sandbox usage only. Requests made with nex_test_ keys still log rows with is_sandbox = true, but those rows are not included in summary, timeseries, by-agent, by-tool, by-key, or outcome totals. See Sandbox (test API keys) for how test keys isolate data elsewhere.

Operator UI

  • Facility dashboard: /admin/api-usage — requires analytics.view; pass facilityId for keys scoped to that facility.
  • Internal platform dashboard: /admin/platform-usage — cross-facility totals (facilityId omitted) for internal operators only; same JSON endpoints and CSV export (GET /api/api-usage/export).

Outcomes

An outcome is a marker the server stamps on a usage row when a call did something billable — it is the unit outcome-based pricing counts, so you need to know which of your calls produce one to reconcile a usage report against your own call log. GET /api/api-usage/outcomes groups usage rows by that marker and returns { data: [{ outcomeType, totalCount }], scope }, largest count first. The outcomeCount field on /summary (and in the CSV export’s summary block) is the number of rows in the same window carrying any marker — because a row carries at most one marker, it equals the sum of totalCount across /outcomes. The other breakdowns (/by-agent, /by-tool, /by-key) do not report an outcome count.

The markers

Four are billing outcome types: Plus one that is recorded but is not a billing outcome type: composio_execution appears in the /outcomes breakdown and counts toward outcomeCount, but it is not one of the four billing outcome types, so it is not reported to the billing meter. Do not read it as a billable unit.

REST calls emit outcomes too

The verbs above are the MCP path. Four REST endpoints mark the same outcomes, so a usage report mixes both surfaces:
A usage row is written only for API-key-authenticated traffic. Session/JWT calls through the app UI hit the same code paths but persist no usage event and therefore no outcome — which is why the dashboards show integration traffic, not in-app activity.

What does and does not produce one

  • Only a successful mutating call. The marker is set after the write commits — a call that is denied, errors, returns a rejection, or pauses at pending_approval produces a usage row with no outcome.
  • Reads never produce one. No read-only verb or registry tool marks an outcome.
  • At most one marker per request. The marker is a single value on the request, so one HTTP request or one tools/call contributes at most one outcome row — later marks overwrite earlier ones rather than accumulating.
swapShifts touches two shifts, and its verb definition is annotated as counting twice, but the runtime records a single shift_filled marker for the call (one marker per request, quantity 1). Reconcile a swap as one shift_filled, not two.

Deduplication over MCP

For MCP tool calls the server derives a dedupe key of mcp:<jsonrpcId>:<toolName> from the JSON-RPC request id and the tool name, and uses it as the billing meter event’s identifier. Retrying the same logical invocation — same JSON-RPC id, same tool — is not metered twice. Use a fresh JSON-RPC id per genuinely new operation and reuse it when retrying, so this works in your favor. There is no equivalent for REST: a REST call with no MCP dedupe key falls back to an identifier derived from its own usage-event row, so two REST calls that both fill a shift are two metered outcomes even if you intended them as one retry. Use an Idempotency-Key on REST mutations — a replayed response never re-executes the write, so it never produces a second outcome. Note that the /outcomes and outcomeCount aggregates count usage rows, not meter events. Deduplication happens on the billing-meter side, so a double-counted retry can appear twice in the analytics breakdown while being billed once.

Sandbox

nex_test_ sandbox traffic is excluded from outcome totals, exactly as it is from every other aggregate on these endpoints: sandbox rows are written with is_sandbox = true and every aggregate filters them out. They are also skipped when reporting to the billing meter, so a sandbox fillShift is never billed. See Sandbox (test API keys).

Response shapes

scope is { "facilityId": 3 } when you pass facilityId, and { "global": true } for a cross-facility internal query. granularity is "day" (the default) or "hour". All endpoints accept days (default 30) to set the window.

Authorizations

Authorization
string
header
required

JWT token authentication

Headers

NexSpace-Version
string

Pin the request to a dated API version (e.g. 2026-05-10). Applied by app-level middleware to every /api route (server/routes/index.ts → apiVersionMiddleware), which echoes the resolved value back in the NexSpace-Version response header. Omit to get the latest version. Discover the catalog at GET /.well-known/api-versions.

Pattern: ^\d{4}-\d{2}-\d{2}$

Query Parameters

facilityId
integer

When set, restrict aggregates to API keys with this api_keys.facility_id. Required for facility-scoped sessions that are not internal cross-facility operators. Omit for platform-wide totals (internal operators only).

days
integer
default:30

Lookback period in days

Response

Usage summary

totalCalls
integer
uniqueKeys
integer
avgLatencyMs
integer
p95LatencyMs
integer
errorCount
integer
outcomeCount
integer
periodDays
integer

Echo of the days query parameter (default 30).

from
string<date-time>

Start of the aggregation window (ISO 8601).

to
string<date-time>

End of the aggregation window (ISO 8601).

scope
object

Which slice of usage the aggregate covers. Facility-scoped sessions get { facilityId }; internal cross-facility operators querying without a facilityId get { global: true }.