Usage API
One read-only call answers "how much, by whom, on what": requests, outcomes, tokens and cost of every app in your organization, over all time or any range. Rows are per period and optionally per one or two dimensions, and totals always covers the whole selection.
No extra scope is needed: a key with read can call all three. Try them with your own key in the Management API playground.
Build a report
Filters narrow every number, including totals, and totals always equals the sum of every row across all pages. Periods without traffic are omitted, so a chart should fill gaps with zero.
What the numbers mean
- Dimensions.
appuses the app's current name while the app exists, otherwise its recorded name;app_deleted: truemarks an app that no longer exists.modelis the served model (after routing and aliases), not the model name the client sent.keyis a gateway credential id (withkey_namewhile the credential exists) andprovideris the provider label. A value isnullwhen it was not recorded, anduserisnullwhen the request carried no identity or data retention removed it. - Freshness. Results can be up to 60 seconds old.
generated_atis when the data was read, anddata_throughis the effective end of the range (yourto, or the time of the read). Requests are written a few seconds to minutes after they finish, so the latest ones can be missing. - Paging. A
next_cursoris tied to the result it came from. If the result is recomputed and has changed (new or late requests), the next page answers409 collection_changed: restart withoutcursor. For a stable export, pass atoin the past. - Cost of a query. Grouping or filtering by
keyreads more data. Use a bounded range with it. Queries are limited to 20 seconds and 100,000 result rows (422 result_too_large). - Errors. An invalid parameter returns
400 invalid_requestwith a message such asto must be after fromorgroup_by accepts at most 2 dimensions.503 query_timeoutmeans the query exceeded its time or read limits: narrow it and retry.503 data_unavailablemeans usage data cannot be read right now.
This API returns aggregates. To pull every request log line into your warehouse or SIEM, use the Log Export API. To understand one request, use Explain a request.
Admin recipes
Each recipe needs a management key with read.
export API="$MANAGEMENT_URL/llmgateway/management/v1"
auth=(-H "Authorization: Bearer $MANAGEMENT_KEY")
# Every row of a usage report, one JSON object per line: follows next_cursor to the
# last page and stops on an error (for example 409 collection_changed: rerun it).
usage_rows() {
local cursor="" page
while :; do
page=$(curl -sG "$API/usage?$1" "${auth[@]}" ${cursor:+--data-urlencode "cursor=$cursor"}) || return 1
if jq -e '.error' >/dev/null <<<"$page"; then jq -c '.error' <<<"$page" >&2; return 1; fi
jq -c '.rows[]' <<<"$page"
cursor=$(jq -r '.next_cursor // empty' <<<"$page")
[ -n "$cursor" ] || return 0
done
}
The recipes that export a report use usage_rows, so they include every page. A recipe that reads one request is labeled "first page".
Daily spend for the last 30 days
FROM=$(date -u -d '30 days ago' +%F 2>/dev/null || date -u -v-30d +%F)
usage_rows "from=$FROM&granularity=day" | jq -r '[.period_start, .requests, .cost_usd] | @tsv'
All-time spend by app
usage_rows "granularity=total&group_by=app&sort=cost" \
| jq -r '[.app, .requests, .cost_usd, .unpriced_requests] | @tsv'
Top 10 users this month
First page only: limit=10 with sort=cost returns the ten highest-cost users.
curl -s "$API/usage?from=$(date -u +%Y-%m-01)&granularity=total&group_by=user&sort=cost&limit=10" "${auth[@]}" \
| jq -r '.rows[] | [.user // "(no identity)", .requests, .cost_usd] | @tsv'
Spend by model per week, for one app
usage_rows "from=2026-09-01&granularity=week&group_by=model&app=Support%20Bot" \
| jq -r '[.period_start, .model, .cost_usd] | @tsv'
Find stale keys
Every key in a credentials list or read carries usage: last_used_at (the most recent request in the last 90 days, or null when there was none) and requests_30d (requests in the last 30 days, 0 when there were none). List the active keys of an app that nobody used in the last 90 days:
First page only (up to 50 keys); follow next_cursor for apps with more.
curl -s "$API/apps/Support%20Bot/keys" "${auth[@]}" \
| jq -r '.items[] | select(.status == "active" and .usage != null and .usage.last_used_at == null)
| [.key_id, .name, .created_at] | @tsv'
usage itself is null only when usage data is unavailable, so the filter skips those keys instead of reporting them as stale. To compare keys across every app over a longer range, group usage by key: GET /usage?from=2026-01-01&granularity=total&group_by=app,key&sort=requests. Revoke a stale key with Revoke a gateway credential.
Error and block rate by app
usage_rows "from=$FROM&granularity=total&group_by=app,outcome" | jq -r '[.app, .outcome, .requests] | @tsv'
For why requests failed at a provider, read provider health.
Endpoints
/usageRead usage
Requests, outcomes, tokens and cost of every app in the tenant, over all time by default (`from` inclusive, `to` exclusive; a date `to` includes that whole UTC day). Rows are per period (`granularity`: total by default, or UTC month, week starting Monday, or day) and optionally per one or two dimensions: app, model, provider, user, key (gateway credential), source or outcome. Filters narrow every number, including totals, and `totals` always equals the sum of every row across all pages. Cost is the cost recorded with each request at request time; requests without one are counted in unpriced_requests instead of being estimated. Request counts exclude the background cache-refresh requests the gateway sends on its own, while tokens and cost include them because the provider bills them. Requests whose user identity was removed by data retention are kept with user null. Periods without traffic are omitted. Credential ids are recorded from 2026-10-16; earlier requests report key null. Grouping or filtering by key reads more data, so prefer a bounded range with it. Results can be up to 60 seconds old. Page with `limit` and `cursor` while the query stays the same; a cursor answers 409 `collection_changed` when the result was recomputed and differs (restart without cursor; a fixed `to` in the past keeps results stable). Rows group by the app's current name while it exists, otherwise the name recorded with its requests, and by the served model. `generated_at` says when the data was read and `data_through` the effective end of the range; `totals.priced_coverage_pct` says how much of the billable traffic has a recorded cost. Queries are limited to {QUERY_TIMEOUT_SECONDS} seconds and 100,000 result rows (422 `result_too_large`). 503 `query_timeout` when the query exceeds its time or read limits (narrow it and retry); 503 `data_unavailable` when the data cannot be read right now.
readPath, query & header parameters 14
from (query)stringoptionalInclusive start: a date (YYYY-MM-DD, UTC midnight) or an RFC 3339 timestamp with a timezone. Omitted: from the first recorded request.
to (query)stringoptionalExclusive end: an RFC 3339 timestamp with a timezone, or a date (YYYY-MM-DD) to include that whole UTC day. Omitted: now.
granularity (query)stringoptionalPeriod of each row: total (one row per group), or UTC month, week (starting Monday) or day.
"total""month""week""day"group_by (query)array<string>optionalUp to 2 of: app, model, provider, user, key, source, outcome. Repeat the parameter (group_by=a&group_by=b) or separate values with commas (group_by=a,b).
Array item specification
stringValues: "app""model""provider""user""key""source""outcome"
app (query)stringoptionalOnly these apps (names, case-insensitive). Comma-separated, at most 50 values.
model (query)stringoptionalOnly these models. Comma-separated, at most 50 values.
provider (query)stringoptionalOnly these provider labels. Comma-separated, at most 50 values.
user (query)stringoptionalOnly these user emails (case-insensitive). Comma-separated, at most 50 values.
key (query)stringoptionalOnly these gateway credential ids (key_id). Comma-separated, at most 50 values.
source (query)stringoptionalOnly these sources: gateway, sdk, copilot_studio. Comma-separated, at most 50 values.
outcome (query)stringoptionalOnly these outcomes: served, blocked, error, rate_limited. Comma-separated, at most 50 values.
sort (query)stringoptionalperiod: oldest period first, then highest cost. cost, requests, tokens: highest first.
"period""cost""requests""tokens"limit (query)integeroptionalPage size: 1 to 5000, default 500.
cursor (query)stringoptionalnext_cursor from the previous page; send the same other parameters.
No request body.
curl --request GET \
'https://guardrails-usa-1.quilr.ai/llmgateway/management/v1/usage?limit=500' \
--header 'Authorization: Bearer <management-key>'
Authentication, errors & retry rules Try it in the playground
Related
- Monitoring API - guardrail activity, provider health and single-request explanations.
- Users API - per-person access, keys and usage.
- Budgets API - enforce spend, request and token limits.
- Log Export API - every request log line, for your own data platform.
- Management API - authentication, errors and retry rules.