> For the complete documentation index, see [llms.txt](https://docs.otari.ai/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.otari.ai/otari/for-integrators/api-reference/usage.md).

# Usage

## List Usage

> List usage logs ordered by timestamp (most recent first).\
> \
> Supports optional filters for time range, user, status, failure status code,\
> model, endpoint, provider, source, session (\`\`source\_label\`\`), and request\
> group (\`\`request\_group\_id\`\`, repeatable, which returns a routed request's\
> whole attempt plan). Paginated via skip/limit. The return shape is a bare JSON array; external\
> billing/analytics consumers depend on this, so the total row count for a\
> paginated UI is served separately by \`\`GET /api/v1/usage/count\`\` rather than\
> wrapped in an envelope here. Timestamps accept either ISO 8601 strings or\
> Unix epoch seconds (numeric).

```json
{"openapi":"3.1.0","info":{"title":"otari","version":"0.33.0"},"security":[{"ApiKeyAuth":[]},{"XApiKeyAuth":[]}],"components":{"securitySchemes":{"ApiKeyAuth":{"description":"Enter your API key here (sent as Otari-Key header).","in":"header","name":"Otari-Key","type":"apiKey"},"XApiKeyAuth":{"description":"Anthropic-native clients send credentials here (no Bearer prefix).","in":"header","name":"x-api-key","type":"apiKey"}},"schemas":{"UsageEntry":{"description":"A single usage log entry.","properties":{"api_key_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Api Key Id"},"api_key_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Api Key Name"},"attempt_count":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Attempt Count"},"attempt_position":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Attempt Position"},"billing_meters":{"anyOf":[{"$ref":"#/components/schemas/BillingMeters"},{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Billing Meters"},"bulk_editable":{"title":"Bulk Editable","type":"boolean"},"cache_read_tokens":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Cache Read Tokens"},"cache_write_1h_tokens":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Cache Write 1H Tokens"},"cache_write_tokens":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Cache Write Tokens"},"completion_tokens":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Completion Tokens"},"cost":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Cost"},"counts_toward_budget":{"title":"Counts Toward Budget","type":"boolean"},"endpoint":{"title":"Endpoint","type":"string"},"error_message":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Error Message"},"id":{"title":"Id","type":"string"},"latency_ms":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Latency Ms"},"model":{"title":"Model","type":"string"},"policy_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Policy Name"},"pricing_breakdown":{"anyOf":[{"items":{"anyOf":[{"$ref":"#/components/schemas/ToolChargeLine"},{"$ref":"#/components/schemas/TokenChargeLine"},{"$ref":"#/components/schemas/UnitChargeLine"},{"additionalProperties":true,"type":"object"}]},"type":"array"},{"type":"null"}],"title":"Pricing Breakdown"},"prompt_tokens":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Prompt Tokens"},"provider":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Provider"},"provider_latency_ms":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Provider Latency Ms"},"reasoning_tokens":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Reasoning Tokens"},"request_group_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Request Group Id"},"selection_reason":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Selection Reason"},"source":{"title":"Source","type":"string"},"source_label":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Source Label"},"status":{"title":"Status","type":"string"},"status_code":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Status Code"},"tags":{"anyOf":[{"additionalProperties":{"type":"string"},"type":"object"},{"type":"null"}],"title":"Tags"},"timestamp":{"title":"Timestamp","type":"string"},"total_tokens":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Total Tokens"},"user_alias":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"User Alias"},"user_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"User Id"}},"required":["id","user_id","api_key_id","timestamp","model","provider","endpoint","prompt_tokens","completion_tokens","total_tokens","cache_read_tokens","cache_write_tokens","cache_write_1h_tokens","billing_meters","pricing_breakdown","cost","status","error_message","status_code","latency_ms","provider_latency_ms","source","source_label","counts_toward_budget","bulk_editable"],"title":"UsageEntry","type":"object"},"BillingMeters":{"additionalProperties":true,"description":"The meters a request was billed on.\n\nToken meters sit flat (``{\"input\": 1200}``), which is why extra keys are\nallowed: the names come from the provider and the endpoint, so the map is\ngenuinely open. Gateway-run tool counts are the one reserved key, nested\nunder ``tools`` because an MCP server can advertise a tool named after a\ntoken meter and a flat collision would corrupt the billed-token aggregates\nfor the whole window (see ``gateway.services.tool_usage``).\n\nA TypedDict for the reason above, and for one that cost a round trip to\nlearn: a model that suppressed its defaults with a ``model_serializer``\npublished an *empty* schema, because that serializer replaces the\nserialization JSON schema and that is the one FastAPI puts in the spec. The\nproperties below are the whole point, so they have to survive into\ndocs/public/openapi.json; ``tests/unit/test_billing_schemas.py`` asserts they\ndo.","properties":{"tools":{"additionalProperties":{"$ref":"#/components/schemas/ToolMeter"},"title":"Tools","type":"object"}},"title":"BillingMeters","type":"object"},"ToolMeter":{"additionalProperties":true,"description":"One gateway-run tool's call counts on a request.\n\nA TypedDict rather than a model, and every key optional, so a stored entry\nserializes back as it was written: a model with defaults would put\n``\"unit_rate\": null`` on every tool that has never been priced.","properties":{"billed":{"title":"Billed","type":"integer"},"errors":{"title":"Errors","type":"integer"},"unit_rate":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Unit Rate"}},"title":"ToolMeter","type":"object"},"ToolChargeLine":{"additionalProperties":true,"description":"A gateway-run tool's charge line, one per tool, label and payer.\n\nA subclass with ``label`` required, rather than a defaulted field on\n:class:`UnitChargeLine`, so the per-request, rerank and image lines, and a\nstored tool line without a label, serialize back without a\n``\"label\": null`` they never had.\n\nA line written since tools are charged by whose account ran them also\ncarries ``deployment_paid``: true where the deployment's account ran the\ncalls, which charges the organization's funds where the deployment bills\nthem, and false where the organization's own key did. It is kept as an extra\nkey rather than declared, so an older line serializes back without it.","properties":{"cost":{"description":"USD charged for this line.","title":"Cost","type":"number"},"label":{"description":"What served the calls: '<provider>:<instance>' for a search or fetch instance, 'builtin:builtin_fetch' for the built-in fetcher, 'org:<provider>' for an organization's own key, or 'otari:<tool>' for any other gateway-run tool.","title":"Label","type":"string"},"meter":{"description":"What was metered, e.g. 'request' or 'web_search_calls'.","title":"Meter","type":"string"},"unit_rate":{"description":"USD per call.","title":"Unit Rate","type":"number"},"units":{"anyOf":[{"type":"integer"},{"type":"number"}],"description":"Calls billed on this meter.","title":"Units"}},"required":["meter","units","unit_rate","cost","label"],"title":"ToolChargeLine","type":"object"},"TokenChargeLine":{"additionalProperties":true,"description":"A charge line billed per million tokens.\n\n``rate_per_million`` is the discriminator: its presence (rather than\n``unit_rate``) is what tells a reader, and a renderer, which unit convention\napplies. See ``gateway.services.pricing_service``.","properties":{"cost":{"description":"USD charged for this line.","title":"Cost","type":"number"},"meter":{"description":"What was metered, e.g. 'input' or 'cache_read'.","title":"Meter","type":"string"},"rate_per_million":{"description":"USD per million units.","title":"Rate Per Million","type":"number"},"units":{"anyOf":[{"type":"integer"},{"type":"number"}],"description":"Tokens billed on this meter.","title":"Units"}},"required":["meter","units","rate_per_million","cost"],"title":"TokenChargeLine","type":"object"},"UnitChargeLine":{"additionalProperties":true,"description":"A charge line billed per call, for gateway-run tools and per-request routes.\n\n``unit_rate`` is the discriminator, in the same sense as\n:class:`TokenChargeLine`'s ``rate_per_million``.","properties":{"cost":{"description":"USD charged for this line.","title":"Cost","type":"number"},"meter":{"description":"What was metered, e.g. 'request' or 'web_search_calls'.","title":"Meter","type":"string"},"unit_rate":{"description":"USD per call.","title":"Unit Rate","type":"number"},"units":{"anyOf":[{"type":"integer"},{"type":"number"}],"description":"Calls billed on this meter.","title":"Units"}},"required":["meter","units","unit_rate","cost"],"title":"UnitChargeLine","type":"object"},"HTTPValidationError":{"properties":{"code":{"const":"invalid_request","description":"Stable error code, also sent as the Otari-Error-Code header.","title":"Code","type":"string"},"detail":{"items":{"$ref":"#/components/schemas/ValidationError"},"title":"Detail","type":"array"}},"title":"HTTPValidationError","type":"object"},"ValidationError":{"properties":{"ctx":{"title":"Context","type":"object"},"input":{"title":"Input"},"loc":{"items":{"anyOf":[{"type":"string"},{"type":"integer"}]},"title":"Location","type":"array"},"msg":{"title":"Message","type":"string"},"type":{"title":"Error Type","type":"string"}},"required":["loc","msg","type"],"title":"ValidationError","type":"object"}}},"paths":{"/api/v1/usage":{"get":{"description":"List usage logs ordered by timestamp (most recent first).\n\nSupports optional filters for time range, user, status, failure status code,\nmodel, endpoint, provider, source, session (``source_label``), and request\ngroup (``request_group_id``, repeatable, which returns a routed request's\nwhole attempt plan). Paginated via skip/limit. The return shape is a bare JSON array; external\nbilling/analytics consumers depend on this, so the total row count for a\npaginated UI is served separately by ``GET /api/v1/usage/count`` rather than\nwrapped in an envelope here. Timestamps accept either ISO 8601 strings or\nUnix epoch seconds (numeric).","operationId":"usage-list_usage","parameters":[{"description":"Return logs with timestamp >= start_date (ISO 8601 or Unix epoch seconds)","in":"query","name":"start_date","required":false,"schema":{"anyOf":[{"format":"date-time","type":"string"},{"type":"null"}],"description":"Return logs with timestamp >= start_date (ISO 8601 or Unix epoch seconds)","title":"Start Date"}},{"description":"Return logs with timestamp < end_date (ISO 8601 or Unix epoch seconds)","in":"query","name":"end_date","required":false,"schema":{"anyOf":[{"format":"date-time","type":"string"},{"type":"null"}],"description":"Return logs with timestamp < end_date (ISO 8601 or Unix epoch seconds)","title":"End Date"}},{"description":"Filter to one or more users; repeatable (user_id=a&user_id=b). Several values match any of them. At most 50 per call.","in":"query","name":"user_id","required":false,"schema":{"anyOf":[{"items":{"type":"string"},"maxItems":50,"type":"array"},{"type":"null"}],"description":"Filter to one or more users; repeatable (user_id=a&user_id=b). Several values match any of them. At most 50 per call.","title":"User Id"}},{"description":"Filter to a single status: 'success', 'error', or 'absorbed' (an attempt a routing policy recovered from, excluded from error_count and request_count)","in":"query","name":"status","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Filter to a single status: 'success', 'error', or 'absorbed' (an attempt a routing policy recovered from, excluded from error_count and request_count)","title":"Status"}},{"description":"Filter to a single failure status code (e.g. 429 for provider rate limits, 402 for missing-pricing rejections). Only error rows carry one, so this filter also restricts to status='error' unless 'status' is given explicitly","in":"query","name":"status_code","required":false,"schema":{"anyOf":[{"type":"integer"},{"type":"null"}],"description":"Filter to a single failure status code (e.g. 429 for provider rate limits, 402 for missing-pricing rejections). Only error rows carry one, so this filter also restricts to status='error' unless 'status' is given explicitly","title":"Status Code"}},{"description":"Filter to one or more models; repeatable (model=a&model=b). Several values match any of them. At most 50 per call.","in":"query","name":"model","required":false,"schema":{"anyOf":[{"items":{"type":"string"},"maxItems":50,"type":"array"},{"type":"null"}],"description":"Filter to one or more models; repeatable (model=a&model=b). Several values match any of them. At most 50 per call.","title":"Model"}},{"description":"Filter to a single endpoint (e.g. '/v1/chat/completions')","in":"query","name":"endpoint","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Filter to a single endpoint (e.g. '/v1/chat/completions')","title":"Endpoint"}},{"description":"Filter to a single provider (e.g. 'openai')","in":"query","name":"provider","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Filter to a single provider (e.g. 'openai')","title":"Provider"}},{"description":"Filter to a single provenance source (e.g. 'gateway' or 'claude_code')","in":"query","name":"source","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Filter to a single provenance source (e.g. 'gateway' or 'claude_code')","title":"Source"}},{"description":"Filter to a single session (source_label): the session's trace id on a gateway request that named one, or the session/project label imported usage carried","in":"query","name":"source_label","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Filter to a single session (source_label): the session's trace id on a gateway request that named one, or the session/project label imported usage carried","title":"Source Label"}},{"description":"Filter by a request tag (what a request sent in its `metadata`), as `key:value`. Repeatable: values for the same key match any of them, and different keys must all match. At most 50 per call.","in":"query","name":"tag","required":false,"schema":{"anyOf":[{"items":{"type":"string"},"maxItems":50,"type":"array"},{"type":"null"}],"description":"Filter by a request tag (what a request sent in its `metadata`), as `key:value`. Repeatable: values for the same key match any of them, and different keys must all match. At most 50 per call.","title":"Tag"}},{"description":"Filter to one or more API key ids; repeatable (api_key_id=a&api_key_id=b). Several values match any of them. At most 50 per call.","in":"query","name":"api_key_id","required":false,"schema":{"anyOf":[{"items":{"type":"string"},"maxItems":50,"type":"array"},{"type":"null"}],"description":"Filter to one or more API key ids; repeatable (api_key_id=a&api_key_id=b). Several values match any of them. At most 50 per call.","title":"Api Key Id"}},{"description":"Filter by token-pricing state: true = only rows whose model tokens were priced, false = only rows that still need pricing (no cost at all, or tokens that were never metered because the model had no rate). A row charged only for gateway-run tool calls still counts as needing pricing.","in":"query","name":"priced","required":false,"schema":{"anyOf":[{"type":"boolean"},{"type":"null"}],"description":"Filter by token-pricing state: true = only rows whose model tokens were priced, false = only rows that still need pricing (no cost at all, or tokens that were never metered because the model had no rate). A row charged only for gateway-run tool calls still counts as needing pricing.","title":"Priced"}},{"description":"Filter to requests that ran a gateway-run tool. 'any' matches any tool; a tool name (web_search, web_fetch, code_execution) matches that tool specifically.","in":"query","name":"tool","required":false,"schema":{"anyOf":[{"enum":["any","web_search","web_fetch","code_execution"],"type":"string"},{"type":"null"}],"description":"Filter to requests that ran a gateway-run tool. 'any' matches any tool; a tool name (web_search, web_fetch, code_execution) matches that tool specifically.","title":"Tool"}},{"description":"Filter by budget participation, which is not the same question as provenance: true = only enforced gateway rows, false = every row that never touches a budget, meaning imported usage and also gateway traffic on a budget-exempt key","in":"query","name":"counts_toward_budget","required":false,"schema":{"anyOf":[{"type":"boolean"},{"type":"null"}],"description":"Filter by budget participation, which is not the same question as provenance: true = only enforced gateway rows, false = every row that never touches a budget, meaning imported usage and also gateway traffic on a budget-exempt key","title":"Counts Toward Budget"}},{"description":"Filter to the rows of one or more request groups; repeatable (request_group_id=a&request_group_id=b). A routed request writes one row per attempt, all sharing a request_group_id, so this returns a request's whole plan: its absorbed attempts and the attempt that served it. Ignore ordering by timestamp and read attempt_position to reconstruct the plan. At most 1000 ids per call.","in":"query","name":"request_group_id","required":false,"schema":{"anyOf":[{"items":{"type":"string"},"maxItems":1000,"type":"array"},{"type":"null"}],"description":"Filter to the rows of one or more request groups; repeatable (request_group_id=a&request_group_id=b). A routed request writes one row per attempt, all sharing a request_group_id, so this returns a request's whole plan: its absorbed attempts and the attempt that served it. Ignore ordering by timestamp and read attempt_position to reconstruct the plan. At most 1000 ids per call.","title":"Request Group Id"}},{"description":"Only usage recorded in this workspace.","in":"query","name":"workspace_id","required":false,"schema":{"anyOf":[{"format":"uuid","type":"string"},{"type":"null"}],"description":"Only usage recorded in this workspace.","title":"Workspace Id"}},{"in":"query","name":"skip","required":false,"schema":{"default":0,"minimum":0,"title":"Skip","type":"integer"}},{"in":"query","name":"limit","required":false,"schema":{"default":100,"maximum":1000,"minimum":1,"title":"Limit","type":"integer"}}],"responses":{"200":{"content":{"application/json":{"schema":{"items":{"$ref":"#/components/schemas/UsageEntry"},"title":"Response Usage-List Usage","type":"array"}}},"description":"Successful Response"},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}},"description":"Validation Error"}},"summary":"List Usage","tags":["usage"]}}}}
```

## Delete Usage Rows

> Delete imported usage rows by explicit ids or by filter.\
> \
> Target either the current selection (\`\`ids\`\`) or everything matching a filter\
> (\`\`by\_filter: true\`\` plus optional \`\`source\`\` / \`\`model\`\` / \`\`user\_id\`\` /\
> \`\`status\`\` / date range / \`\`priced\`\`). Only imported rows\
> (\`\`counts\_toward\_budget = false\`\`) are ever removed: enforced gateway rows and\
> the spend ledger (\`\`users.spend\`\`) are untouched, so a delete can never desync a\
> budget. Master-key only.

```json
{"openapi":"3.1.0","info":{"title":"otari","version":"0.33.0"},"security":[{"ApiKeyAuth":[]},{"XApiKeyAuth":[]}],"components":{"securitySchemes":{"ApiKeyAuth":{"description":"Enter your API key here (sent as Otari-Key header).","in":"header","name":"Otari-Key","type":"apiKey"},"XApiKeyAuth":{"description":"Anthropic-native clients send credentials here (no Bearer prefix).","in":"header","name":"x-api-key","type":"apiKey"}},"schemas":{"UsageDeleteRequest":{"description":"Selection of imported usage rows to delete.","properties":{"api_key_id":{"anyOf":[{"type":"string"},{"items":{"type":"string"},"maxItems":50,"type":"array"},{"type":"null"}],"title":"Api Key Id"},"by_filter":{"default":false,"title":"By Filter","type":"boolean"},"end_date":{"anyOf":[{"format":"date-time","type":"string"},{"type":"null"}],"title":"End Date"},"endpoint":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Endpoint"},"ids":{"anyOf":[{"items":{"type":"string"},"maxItems":1000,"type":"array"},{"type":"null"}],"title":"Ids"},"model":{"anyOf":[{"type":"string"},{"items":{"type":"string"},"maxItems":50,"type":"array"},{"type":"null"}],"title":"Model"},"priced":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Priced"},"provider":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Provider"},"source":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Source"},"source_label":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Source Label"},"start_date":{"anyOf":[{"format":"date-time","type":"string"},{"type":"null"}],"title":"Start Date"},"status":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Status"},"tool":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Tool"},"user_id":{"anyOf":[{"type":"string"},{"items":{"type":"string"},"maxItems":50,"type":"array"},{"type":"null"}],"title":"User Id"},"workspace_id":{"anyOf":[{"format":"uuid","type":"string"},{"type":"null"}],"title":"Workspace Id"}},"title":"UsageDeleteRequest","type":"object"},"UsageDeleteResult":{"description":"How many imported rows the delete removed.","properties":{"deleted":{"default":0,"title":"Deleted","type":"integer"}},"title":"UsageDeleteResult","type":"object"},"HTTPValidationError":{"properties":{"code":{"const":"invalid_request","description":"Stable error code, also sent as the Otari-Error-Code header.","title":"Code","type":"string"},"detail":{"items":{"$ref":"#/components/schemas/ValidationError"},"title":"Detail","type":"array"}},"title":"HTTPValidationError","type":"object"},"ValidationError":{"properties":{"ctx":{"title":"Context","type":"object"},"input":{"title":"Input"},"loc":{"items":{"anyOf":[{"type":"string"},{"type":"integer"}]},"title":"Location","type":"array"},"msg":{"title":"Message","type":"string"},"type":{"title":"Error Type","type":"string"}},"required":["loc","msg","type"],"title":"ValidationError","type":"object"}}},"paths":{"/api/v1/usage":{"delete":{"description":"Delete imported usage rows by explicit ids or by filter.\n\nTarget either the current selection (``ids``) or everything matching a filter\n(``by_filter: true`` plus optional ``source`` / ``model`` / ``user_id`` /\n``status`` / date range / ``priced``). Only imported rows\n(``counts_toward_budget = false``) are ever removed: enforced gateway rows and\nthe spend ledger (``users.spend``) are untouched, so a delete can never desync a\nbudget. Master-key only.","operationId":"usage-delete_usage_rows","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UsageDeleteRequest"}}},"required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UsageDeleteResult"}}},"description":"Successful Response"},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}},"description":"Validation Error"}},"summary":"Delete Usage Rows","tags":["usage"]}}}}
```

## Count Usage

> Total number of usage logs matching the given filters.\
> \
> Serves the dashboard paginator's "N of M" total without changing the bare\
> array contract of \`\`GET /api/v1/usage\`\`. Runs only when the client asks (a\
> separate request), so the \`\`COUNT(\*)\`\` is not paid on every page load. With\
> \`\`counts\_toward\_budget=false\`\` it also backs the "select all N matching this\
> filter" affordance for bulk delete / set-price, which touch imported rows only.\
> \
> That value is the one place this count is narrower than \`\`GET /api/v1/usage\`\`: it\
> also excludes rows this deployment served itself, so the number an operator\
> confirms is the number the mutation can reach. The list still pages the\
> budget-exempt gateway rows it omits.

```json
{"openapi":"3.1.0","info":{"title":"otari","version":"0.33.0"},"security":[{"ApiKeyAuth":[]},{"XApiKeyAuth":[]}],"components":{"securitySchemes":{"ApiKeyAuth":{"description":"Enter your API key here (sent as Otari-Key header).","in":"header","name":"Otari-Key","type":"apiKey"},"XApiKeyAuth":{"description":"Anthropic-native clients send credentials here (no Bearer prefix).","in":"header","name":"x-api-key","type":"apiKey"}},"schemas":{"UsageCount":{"description":"Total number of usage logs matching a set of filters.","properties":{"total":{"title":"Total","type":"integer"}},"required":["total"],"title":"UsageCount","type":"object"},"HTTPValidationError":{"properties":{"code":{"const":"invalid_request","description":"Stable error code, also sent as the Otari-Error-Code header.","title":"Code","type":"string"},"detail":{"items":{"$ref":"#/components/schemas/ValidationError"},"title":"Detail","type":"array"}},"title":"HTTPValidationError","type":"object"},"ValidationError":{"properties":{"ctx":{"title":"Context","type":"object"},"input":{"title":"Input"},"loc":{"items":{"anyOf":[{"type":"string"},{"type":"integer"}]},"title":"Location","type":"array"},"msg":{"title":"Message","type":"string"},"type":{"title":"Error Type","type":"string"}},"required":["loc","msg","type"],"title":"ValidationError","type":"object"}}},"paths":{"/api/v1/usage/count":{"get":{"description":"Total number of usage logs matching the given filters.\n\nServes the dashboard paginator's \"N of M\" total without changing the bare\narray contract of ``GET /api/v1/usage``. Runs only when the client asks (a\nseparate request), so the ``COUNT(*)`` is not paid on every page load. With\n``counts_toward_budget=false`` it also backs the \"select all N matching this\nfilter\" affordance for bulk delete / set-price, which touch imported rows only.\n\nThat value is the one place this count is narrower than ``GET /api/v1/usage``: it\nalso excludes rows this deployment served itself, so the number an operator\nconfirms is the number the mutation can reach. The list still pages the\nbudget-exempt gateway rows it omits.","operationId":"usage-count_usage","parameters":[{"description":"Return logs with timestamp >= start_date (ISO 8601 or Unix epoch seconds)","in":"query","name":"start_date","required":false,"schema":{"anyOf":[{"format":"date-time","type":"string"},{"type":"null"}],"description":"Return logs with timestamp >= start_date (ISO 8601 or Unix epoch seconds)","title":"Start Date"}},{"description":"Return logs with timestamp < end_date (ISO 8601 or Unix epoch seconds)","in":"query","name":"end_date","required":false,"schema":{"anyOf":[{"format":"date-time","type":"string"},{"type":"null"}],"description":"Return logs with timestamp < end_date (ISO 8601 or Unix epoch seconds)","title":"End Date"}},{"description":"Filter to one or more users; repeatable (user_id=a&user_id=b). Several values match any of them. At most 50 per call.","in":"query","name":"user_id","required":false,"schema":{"anyOf":[{"items":{"type":"string"},"maxItems":50,"type":"array"},{"type":"null"}],"description":"Filter to one or more users; repeatable (user_id=a&user_id=b). Several values match any of them. At most 50 per call.","title":"User Id"}},{"description":"Filter to a single status: 'success', 'error', or 'absorbed' (an attempt a routing policy recovered from, excluded from error_count and request_count)","in":"query","name":"status","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Filter to a single status: 'success', 'error', or 'absorbed' (an attempt a routing policy recovered from, excluded from error_count and request_count)","title":"Status"}},{"description":"Filter to a single failure status code (e.g. 429 for provider rate limits, 402 for missing-pricing rejections). Only error rows carry one, so this filter also restricts to status='error' unless 'status' is given explicitly","in":"query","name":"status_code","required":false,"schema":{"anyOf":[{"type":"integer"},{"type":"null"}],"description":"Filter to a single failure status code (e.g. 429 for provider rate limits, 402 for missing-pricing rejections). Only error rows carry one, so this filter also restricts to status='error' unless 'status' is given explicitly","title":"Status Code"}},{"description":"Filter to one or more models; repeatable (model=a&model=b). Several values match any of them. At most 50 per call.","in":"query","name":"model","required":false,"schema":{"anyOf":[{"items":{"type":"string"},"maxItems":50,"type":"array"},{"type":"null"}],"description":"Filter to one or more models; repeatable (model=a&model=b). Several values match any of them. At most 50 per call.","title":"Model"}},{"description":"Filter to a single endpoint (e.g. '/v1/chat/completions')","in":"query","name":"endpoint","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Filter to a single endpoint (e.g. '/v1/chat/completions')","title":"Endpoint"}},{"description":"Filter to a single provider (e.g. 'openai')","in":"query","name":"provider","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Filter to a single provider (e.g. 'openai')","title":"Provider"}},{"description":"Filter to a single provenance source (e.g. 'gateway' or 'claude_code')","in":"query","name":"source","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Filter to a single provenance source (e.g. 'gateway' or 'claude_code')","title":"Source"}},{"description":"Filter to a single session (source_label): the session's trace id on a gateway request that named one, or the session/project label imported usage carried","in":"query","name":"source_label","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Filter to a single session (source_label): the session's trace id on a gateway request that named one, or the session/project label imported usage carried","title":"Source Label"}},{"description":"Filter by a request tag (what a request sent in its `metadata`), as `key:value`. Repeatable: values for the same key match any of them, and different keys must all match. At most 50 per call.","in":"query","name":"tag","required":false,"schema":{"anyOf":[{"items":{"type":"string"},"maxItems":50,"type":"array"},{"type":"null"}],"description":"Filter by a request tag (what a request sent in its `metadata`), as `key:value`. Repeatable: values for the same key match any of them, and different keys must all match. At most 50 per call.","title":"Tag"}},{"description":"Filter to one or more API key ids; repeatable (api_key_id=a&api_key_id=b). Several values match any of them. At most 50 per call.","in":"query","name":"api_key_id","required":false,"schema":{"anyOf":[{"items":{"type":"string"},"maxItems":50,"type":"array"},{"type":"null"}],"description":"Filter to one or more API key ids; repeatable (api_key_id=a&api_key_id=b). Several values match any of them. At most 50 per call.","title":"Api Key Id"}},{"description":"Filter by token-pricing state: true = only rows whose model tokens were priced, false = only rows that still need pricing (no cost at all, or tokens that were never metered because the model had no rate). A row charged only for gateway-run tool calls still counts as needing pricing.","in":"query","name":"priced","required":false,"schema":{"anyOf":[{"type":"boolean"},{"type":"null"}],"description":"Filter by token-pricing state: true = only rows whose model tokens were priced, false = only rows that still need pricing (no cost at all, or tokens that were never metered because the model had no rate). A row charged only for gateway-run tool calls still counts as needing pricing.","title":"Priced"}},{"description":"Filter to requests that ran a gateway-run tool. 'any' matches any tool; a tool name (web_search, web_fetch, code_execution) matches that tool specifically.","in":"query","name":"tool","required":false,"schema":{"anyOf":[{"enum":["any","web_search","web_fetch","code_execution"],"type":"string"},{"type":"null"}],"description":"Filter to requests that ran a gateway-run tool. 'any' matches any tool; a tool name (web_search, web_fetch, code_execution) matches that tool specifically.","title":"Tool"}},{"description":"Filter by budget participation: true = only enforced gateway rows, false = only imported rows, narrowed past the filter of the same name on GET /api/v1/usage so the total matches what bulk delete and set-price can reach","in":"query","name":"counts_toward_budget","required":false,"schema":{"anyOf":[{"type":"boolean"},{"type":"null"}],"description":"Filter by budget participation: true = only enforced gateway rows, false = only imported rows, narrowed past the filter of the same name on GET /api/v1/usage so the total matches what bulk delete and set-price can reach","title":"Counts Toward Budget"}},{"description":"Filter to the rows of one or more request groups; repeatable (request_group_id=a&request_group_id=b). A routed request writes one row per attempt, all sharing a request_group_id, so this returns a request's whole plan: its absorbed attempts and the attempt that served it. Ignore ordering by timestamp and read attempt_position to reconstruct the plan. At most 1000 ids per call.","in":"query","name":"request_group_id","required":false,"schema":{"anyOf":[{"items":{"type":"string"},"maxItems":1000,"type":"array"},{"type":"null"}],"description":"Filter to the rows of one or more request groups; repeatable (request_group_id=a&request_group_id=b). A routed request writes one row per attempt, all sharing a request_group_id, so this returns a request's whole plan: its absorbed attempts and the attempt that served it. Ignore ordering by timestamp and read attempt_position to reconstruct the plan. At most 1000 ids per call.","title":"Request Group Id"}},{"description":"Only usage recorded in this workspace.","in":"query","name":"workspace_id","required":false,"schema":{"anyOf":[{"format":"uuid","type":"string"},{"type":"null"}],"description":"Only usage recorded in this workspace.","title":"Workspace Id"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UsageCount"}}},"description":"Successful Response"},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}},"description":"Validation Error"}},"summary":"Count Usage","tags":["usage"]}}}}
```

## Ingest External Usage

> Ingest a batch of externally-observed usage events.\
> \
> Authenticated with either an API key or the master key. Usage binds to the\
> authenticated principal: an API key attributes to its own user (and stamps its\
> id on the rows); the master key may name any user via \`\`user\_id\`\`. Records\
> subscription-backed usage (e.g. Claude Code) as usage-log rows tagged with their\
> \`\`source\`\`, priced at the effective API rate for each event's timestamp.\
> Imported usage is real cost, but never counts toward budgets or mutates\
> \`\`users.spend\`\` (it is retrospective, so it cannot be reserved). Idempotent by\
> \`\`(source, source\_event\_id)\`\`. The payload is content-free; any\
> prompt/completion/tool field is rejected (422), not stored.

```json
{"openapi":"3.1.0","info":{"title":"otari","version":"0.33.0"},"security":[{"ApiKeyAuth":[]},{"XApiKeyAuth":[]}],"components":{"securitySchemes":{"ApiKeyAuth":{"description":"Enter your API key here (sent as Otari-Key header).","in":"header","name":"Otari-Key","type":"apiKey"},"XApiKeyAuth":{"description":"Anthropic-native clients send credentials here (no Bearer prefix).","in":"header","name":"x-api-key","type":"apiKey"}},"schemas":{"ExternalEventsRequest":{"additionalProperties":false,"description":"A batch of imported usage events sharing a source and default user.\n\n``extra=\"forbid\"`` here mirrors the per-event schema: a stray content field at\nthe batch level (e.g. a top-level ``prompt``) is a 422, not silently ignored.","properties":{"events":{"items":{"$ref":"#/components/schemas/ExternalUsageEvent"},"maxItems":1000,"minItems":1,"title":"Events","type":"array"},"source":{"maxLength":64,"pattern":"^[a-zA-Z0-9._:-]+$","title":"Source","type":"string"},"user_id":{"anyOf":[{"maxLength":256,"minLength":1,"type":"string"},{"type":"null"}],"title":"User Id"}},"required":["source","events"],"title":"ExternalEventsRequest","type":"object"},"ExternalUsageEvent":{"additionalProperties":false,"description":"One imported usage event. Content-free: token counts and metadata only.\n\n``extra=\"forbid\"`` rejects any unexpected field (e.g. a stray ``prompt`` or\n``completion``) with a 422 rather than silently dropping it, so no prompt or\ncompletion text can ever be accepted here.","properties":{"cache_read_tokens":{"default":0,"maximum":2147483647,"minimum":0,"title":"Cache Read Tokens","type":"integer"},"cache_tokens_in_prompt":{"default":false,"title":"Cache Tokens In Prompt","type":"boolean"},"cache_write_1h_tokens":{"default":0,"maximum":2147483647,"minimum":0,"title":"Cache Write 1H Tokens","type":"integer"},"cache_write_tokens":{"default":0,"maximum":2147483647,"minimum":0,"title":"Cache Write Tokens","type":"integer"},"duration_ms":{"anyOf":[{"maximum":2147483647,"minimum":0,"type":"integer"},{"type":"null"}],"title":"Duration Ms"},"input_tokens":{"default":0,"maximum":2147483647,"minimum":0,"title":"Input Tokens","type":"integer"},"model":{"maxLength":256,"minLength":1,"pattern":"^[A-Za-z0-9._:/\\-]+$","title":"Model","type":"string"},"output_tokens":{"default":0,"maximum":2147483647,"minimum":0,"title":"Output Tokens","type":"integer"},"provider":{"maxLength":128,"minLength":1,"pattern":"^[A-Za-z0-9._:/\\-]+$","title":"Provider","type":"string"},"reasoning_tokens":{"default":0,"maximum":2147483647,"minimum":0,"title":"Reasoning Tokens","type":"integer"},"session_label":{"anyOf":[{"maxLength":256,"pattern":"^[A-Za-z0-9._:/\\-]+$","type":"string"},{"type":"null"}],"title":"Session Label"},"source_event_id":{"maxLength":256,"minLength":1,"pattern":"^[A-Za-z0-9._:/\\-]+$","title":"Source Event Id","type":"string"},"status":{"default":"success","pattern":"^(success|error)$","title":"Status","type":"string"},"timestamp":{"format":"date-time","title":"Timestamp","type":"string"},"user_id":{"anyOf":[{"maxLength":256,"type":"string"},{"type":"null"}],"title":"User Id"}},"required":["source_event_id","timestamp","provider","model"],"title":"ExternalUsageEvent","type":"object"},"ExternalIngestResult":{"description":"Per-batch outcome. Re-submitting is safe: prior events count as duplicates.","properties":{"accepted":{"default":0,"title":"Accepted","type":"integer"},"duplicate":{"default":0,"title":"Duplicate","type":"integer"},"errors":{"items":{"$ref":"#/components/schemas/ExternalIngestError"},"title":"Errors","type":"array"},"rejected":{"default":0,"title":"Rejected","type":"integer"}},"title":"ExternalIngestResult","type":"object"},"ExternalIngestError":{"description":"A single rejected event, with enough context to fix it and retry.","properties":{"detail":{"title":"Detail","type":"string"},"index":{"title":"Index","type":"integer"},"source_event_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Source Event Id"}},"required":["index","source_event_id","detail"],"title":"ExternalIngestError","type":"object"},"HTTPValidationError":{"properties":{"code":{"const":"invalid_request","description":"Stable error code, also sent as the Otari-Error-Code header.","title":"Code","type":"string"},"detail":{"items":{"$ref":"#/components/schemas/ValidationError"},"title":"Detail","type":"array"}},"title":"HTTPValidationError","type":"object"},"ValidationError":{"properties":{"ctx":{"title":"Context","type":"object"},"input":{"title":"Input"},"loc":{"items":{"anyOf":[{"type":"string"},{"type":"integer"}]},"title":"Location","type":"array"},"msg":{"title":"Message","type":"string"},"type":{"title":"Error Type","type":"string"}},"required":["loc","msg","type"],"title":"ValidationError","type":"object"}}},"paths":{"/api/v1/usage/external-events":{"post":{"description":"Ingest a batch of externally-observed usage events.\n\nAuthenticated with either an API key or the master key. Usage binds to the\nauthenticated principal: an API key attributes to its own user (and stamps its\nid on the rows); the master key may name any user via ``user_id``. Records\nsubscription-backed usage (e.g. Claude Code) as usage-log rows tagged with their\n``source``, priced at the effective API rate for each event's timestamp.\nImported usage is real cost, but never counts toward budgets or mutates\n``users.spend`` (it is retrospective, so it cannot be reserved). Idempotent by\n``(source, source_event_id)``. The payload is content-free; any\nprompt/completion/tool field is rejected (422), not stored.","operationId":"usage-ingest_external_usage","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ExternalEventsRequest"}}},"required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ExternalIngestResult"}}},"description":"Successful Response"},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}},"description":"Validation Error"}},"summary":"Ingest External Usage","tags":["usage"]}}}}
```

## List In Flight

> Requests the gateway is currently serving, longest-running first.\
> \
> A usage row is written when a request settles, so the log alone cannot answer\
> "is anything happening right now": on a slow backend, a 30-second local model\
> call is invisible until it finishes. This reports what is in progress.\
> \
> Read from an in-memory registry, so it describes the process that answers this\
> call and not the deployment: behind a load balancer, consecutive polls reach\
> different otari processes, and there is no deployment-wide total to ask for.\
> \`\`total\`\` is the true in-flight count for the answering process even when\
> \`\`requests\`\` is capped.

```json
{"openapi":"3.1.0","info":{"title":"otari","version":"0.33.0"},"security":[{"ApiKeyAuth":[]},{"XApiKeyAuth":[]}],"components":{"securitySchemes":{"ApiKeyAuth":{"description":"Enter your API key here (sent as Otari-Key header).","in":"header","name":"Otari-Key","type":"apiKey"},"XApiKeyAuth":{"description":"Anthropic-native clients send credentials here (no Bearer prefix).","in":"header","name":"x-api-key","type":"apiKey"}},"schemas":{"InFlightResponse":{"description":"The requests in flight on the answering worker.","properties":{"requests":{"items":{"$ref":"#/components/schemas/InFlightEntry"},"title":"Requests","type":"array"},"total":{"title":"Total","type":"integer"}},"required":["requests","total"],"title":"InFlightResponse","type":"object"},"InFlightEntry":{"description":"One request the gateway is serving right now.\n\nField names match their ``UsageEntry`` counterparts so a request reads the\nsame way in flight as it does once it has settled. ``id`` is the exception: it\nis an ephemeral tracking id, not the id of the usage row this will become.","properties":{"api_key_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Api Key Id"},"elapsed_ms":{"title":"Elapsed Ms","type":"integer"},"endpoint":{"title":"Endpoint","type":"string"},"id":{"title":"Id","type":"string"},"model":{"title":"Model","type":"string"},"policy_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Policy Name"},"provider":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Provider"},"started_at":{"format":"date-time","title":"Started At","type":"string"},"user_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"User Id"}},"required":["id","endpoint","model","provider","user_id","api_key_id","policy_name","started_at","elapsed_ms"],"title":"InFlightEntry","type":"object"}}},"paths":{"/api/v1/usage/in-flight":{"get":{"description":"Requests the gateway is currently serving, longest-running first.\n\nA usage row is written when a request settles, so the log alone cannot answer\n\"is anything happening right now\": on a slow backend, a 30-second local model\ncall is invisible until it finishes. This reports what is in progress.\n\nRead from an in-memory registry, so it describes the process that answers this\ncall and not the deployment: behind a load balancer, consecutive polls reach\ndifferent otari processes, and there is no deployment-wide total to ask for.\n``total`` is the true in-flight count for the answering process even when\n``requests`` is capped.","operationId":"usage-list_in_flight","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/InFlightResponse"}}},"description":"Successful Response"}},"summary":"List In Flight","tags":["usage"]}}}}
```

## Get Request Settlement

> Look up a request's settled cost by the \`\`Otari-Request-ID\`\` it was sent.\
> \
> This is how a caller recovers the cost of a request whose response never\
> carried one: a stream that failed or was disconnected mid-response, or a\
> request that errored. An API key sees only the requests it made; the master\
> key sees any. Returns 404 until the request has settled (its rows are written\
> by a background writer, so a lookup made the instant a stream closes can\
> precede them), and for an id that is unknown or belongs to another key, with\
> no way to tell those apart. A stream that ended before the provider reported\
> usage, and ran no gateway tools, writes no row, so its id stays 404.

```json
{"openapi":"3.1.0","info":{"title":"otari","version":"0.33.0"},"security":[{"ApiKeyAuth":[]},{"XApiKeyAuth":[]}],"components":{"securitySchemes":{"ApiKeyAuth":{"description":"Enter your API key here (sent as Otari-Key header).","in":"header","name":"Otari-Key","type":"apiKey"},"XApiKeyAuth":{"description":"Anthropic-native clients send credentials here (no Bearer prefix).","in":"header","name":"x-api-key","type":"apiKey"}},"schemas":{"RequestSettlement":{"description":"What one request settled at, summed over every usage row it wrote.\n\nA routed request writes a row per attempt and a vision-normalized one a row for\nthe describe call, all sharing the ``Otari-Request-ID`` the caller was sent as\ntheir ``request_group_id``, so this is the request's whole bill rather than one\nattempt's. ``cost_usd`` uses the inline ``usage.cost_usd`` format and is null\nwhen no row was priced.","properties":{"completion_tokens":{"title":"Completion Tokens","type":"integer"},"cost_usd":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Cost Usd"},"prompt_tokens":{"title":"Prompt Tokens","type":"integer"},"request_id":{"title":"Request Id","type":"string"},"row_count":{"title":"Row Count","type":"integer"},"status":{"enum":["success","error"],"title":"Status","type":"string"},"total_tokens":{"title":"Total Tokens","type":"integer"}},"required":["request_id","status","cost_usd","prompt_tokens","completion_tokens","total_tokens","row_count"],"title":"RequestSettlement","type":"object"},"HTTPValidationError":{"properties":{"code":{"const":"invalid_request","description":"Stable error code, also sent as the Otari-Error-Code header.","title":"Code","type":"string"},"detail":{"items":{"$ref":"#/components/schemas/ValidationError"},"title":"Detail","type":"array"}},"title":"HTTPValidationError","type":"object"},"ValidationError":{"properties":{"ctx":{"title":"Context","type":"object"},"input":{"title":"Input"},"loc":{"items":{"anyOf":[{"type":"string"},{"type":"integer"}]},"title":"Location","type":"array"},"msg":{"title":"Message","type":"string"},"type":{"title":"Error Type","type":"string"}},"required":["loc","msg","type"],"title":"ValidationError","type":"object"}}},"paths":{"/api/v1/usage/requests/{request_id}":{"get":{"description":"Look up a request's settled cost by the ``Otari-Request-ID`` it was sent.\n\nThis is how a caller recovers the cost of a request whose response never\ncarried one: a stream that failed or was disconnected mid-response, or a\nrequest that errored. An API key sees only the requests it made; the master\nkey sees any. Returns 404 until the request has settled (its rows are written\nby a background writer, so a lookup made the instant a stream closes can\nprecede them), and for an id that is unknown or belongs to another key, with\nno way to tell those apart. A stream that ended before the provider reported\nusage, and ran no gateway tools, writes no row, so its id stays 404.","operationId":"usage-get_request_settlement","parameters":[{"in":"path","name":"request_id","required":true,"schema":{"maxLength":255,"title":"Request Id","type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RequestSettlement"}}},"description":"Successful Response"},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}},"description":"Validation Error"}},"summary":"Get Request Settlement","tags":["usage"]}}}}
```

## Usage Series

> Time series split by one dimension, for the dashboard's stacked charts.\
> \
> Same filters and window bounds as \`\`/summary\`\` (kept in lockstep: the\
> dashboard serializes one filter object for both, and a filter this endpoint\
> silently ignored would make the stacked chart disagree with the tiles beside\
> it). The window's top groups by spend are returned as their own series;\
> everything past the top eight folds into a single \`\`other\`\` series per\
> bucket, so the stack always reconciles with the summary totals. Points are\
> sparse (populated cells only); the bucket grid is bounded like \`\`/summary\`\`'s\
> series, so an hourly bucket over a too-wide window is rejected rather than\
> ballooning the payload.

```json
{"openapi":"3.1.0","info":{"title":"otari","version":"0.33.0"},"security":[{"ApiKeyAuth":[]},{"XApiKeyAuth":[]}],"components":{"securitySchemes":{"ApiKeyAuth":{"description":"Enter your API key here (sent as Otari-Key header).","in":"header","name":"Otari-Key","type":"apiKey"},"XApiKeyAuth":{"description":"Anthropic-native clients send credentials here (no Bearer prefix).","in":"header","name":"x-api-key","type":"apiKey"}},"schemas":{"UsageGroupedSeries":{"description":"A per-group time series for the dashboard's stacked charts.\n\n``groups`` ranks the window's top groups by spend (plus the reconciling\n``other`` fold), in the order a chart should stack and color them; ``points``\nis sparse (only populated cells), keyed by canonical UTC ``bucket_start``.","properties":{"bucket":{"enum":["hour","day"],"title":"Bucket","type":"string"},"end_date":{"title":"End Date","type":"string"},"group_by":{"enum":["model","user_id","api_key_id","source"],"title":"Group By","type":"string"},"groups":{"items":{"$ref":"#/components/schemas/UsageGroupRow"},"title":"Groups","type":"array"},"points":{"items":{"$ref":"#/components/schemas/UsageGroupedSeriesPoint"},"title":"Points","type":"array"},"start_date":{"title":"Start Date","type":"string"}},"required":["start_date","end_date","bucket","group_by","groups","points"],"title":"UsageGroupedSeries","type":"object"},"UsageGroupRow":{"description":"One breakdown row (a model, a user, an API key, a session, ...).\n\n``key`` is None both for the synthesized fold row (``is_other=True``) and for a\nreal group whose column was NULL (e.g. usage from a since-deleted user, with\n``is_other=False``). ``is_other`` disambiguates the two so the UI does not\nmislabel deleted-user usage as the fold.","properties":{"cost":{"title":"Cost","type":"number"},"is_other":{"default":false,"title":"Is Other","type":"boolean"},"key":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Key"},"label":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Label"},"requests":{"title":"Requests","type":"integer"},"tokens":{"title":"Tokens","type":"integer"}},"required":["key","cost","tokens","requests"],"title":"UsageGroupRow","type":"object"},"UsageGroupedSeriesPoint":{"description":"One (time bucket, group) cell of a grouped series.\n\n``key``/``is_other`` follow the ``UsageGroupRow`` convention: ``key=None``\nwith ``is_other=True`` is the fold of groups outside the top N, ``key=None``\nwith ``is_other=False`` is a real NULL group (e.g. a deleted user).\n``tokens`` is the *billed* total (input including cache, plus output), the\nsame quantity the ungrouped series' composition fields sum to.","properties":{"bucket_start":{"title":"Bucket Start","type":"string"},"cost":{"title":"Cost","type":"number"},"is_other":{"default":false,"title":"Is Other","type":"boolean"},"key":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Key"},"requests":{"title":"Requests","type":"integer"},"tokens":{"title":"Tokens","type":"integer"}},"required":["bucket_start","key","cost","tokens","requests"],"title":"UsageGroupedSeriesPoint","type":"object"},"HTTPValidationError":{"properties":{"code":{"const":"invalid_request","description":"Stable error code, also sent as the Otari-Error-Code header.","title":"Code","type":"string"},"detail":{"items":{"$ref":"#/components/schemas/ValidationError"},"title":"Detail","type":"array"}},"title":"HTTPValidationError","type":"object"},"ValidationError":{"properties":{"ctx":{"title":"Context","type":"object"},"input":{"title":"Input"},"loc":{"items":{"anyOf":[{"type":"string"},{"type":"integer"}]},"title":"Location","type":"array"},"msg":{"title":"Message","type":"string"},"type":{"title":"Error Type","type":"string"}},"required":["loc","msg","type"],"title":"ValidationError","type":"object"}}},"paths":{"/api/v1/usage/series":{"get":{"description":"Time series split by one dimension, for the dashboard's stacked charts.\n\nSame filters and window bounds as ``/summary`` (kept in lockstep: the\ndashboard serializes one filter object for both, and a filter this endpoint\nsilently ignored would make the stacked chart disagree with the tiles beside\nit). The window's top groups by spend are returned as their own series;\neverything past the top eight folds into a single ``other`` series per\nbucket, so the stack always reconciles with the summary totals. Points are\nsparse (populated cells only); the bucket grid is bounded like ``/summary``'s\nseries, so an hourly bucket over a too-wide window is rejected rather than\nballooning the payload.","operationId":"usage-usage_series","parameters":[{"description":"Dimension to split the series by","in":"query","name":"group_by","required":true,"schema":{"description":"Dimension to split the series by","enum":["model","user_id","api_key_id","source"],"title":"Group By","type":"string"}},{"description":"Return logs with timestamp >= start_date (ISO 8601 or Unix epoch seconds)","in":"query","name":"start_date","required":false,"schema":{"anyOf":[{"format":"date-time","type":"string"},{"type":"null"}],"description":"Return logs with timestamp >= start_date (ISO 8601 or Unix epoch seconds)","title":"Start Date"}},{"description":"Return logs with timestamp < end_date (ISO 8601 or Unix epoch seconds)","in":"query","name":"end_date","required":false,"schema":{"anyOf":[{"format":"date-time","type":"string"},{"type":"null"}],"description":"Return logs with timestamp < end_date (ISO 8601 or Unix epoch seconds)","title":"End Date"}},{"description":"Filter to one or more users; repeatable (user_id=a&user_id=b). Several values match any of them. At most 50 per call.","in":"query","name":"user_id","required":false,"schema":{"anyOf":[{"items":{"type":"string"},"maxItems":50,"type":"array"},{"type":"null"}],"description":"Filter to one or more users; repeatable (user_id=a&user_id=b). Several values match any of them. At most 50 per call.","title":"User Id"}},{"description":"Filter to a single status: 'success', 'error', or 'absorbed' (an attempt a routing policy recovered from, excluded from error_count and request_count)","in":"query","name":"status","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Filter to a single status: 'success', 'error', or 'absorbed' (an attempt a routing policy recovered from, excluded from error_count and request_count)","title":"Status"}},{"description":"Filter to a single failure status code (e.g. 429 for provider rate limits, 402 for missing-pricing rejections). Only error rows carry one, so this filter also restricts to status='error' unless 'status' is given explicitly","in":"query","name":"status_code","required":false,"schema":{"anyOf":[{"type":"integer"},{"type":"null"}],"description":"Filter to a single failure status code (e.g. 429 for provider rate limits, 402 for missing-pricing rejections). Only error rows carry one, so this filter also restricts to status='error' unless 'status' is given explicitly","title":"Status Code"}},{"description":"Filter to one or more models; repeatable (model=a&model=b). Several values match any of them. At most 50 per call.","in":"query","name":"model","required":false,"schema":{"anyOf":[{"items":{"type":"string"},"maxItems":50,"type":"array"},{"type":"null"}],"description":"Filter to one or more models; repeatable (model=a&model=b). Several values match any of them. At most 50 per call.","title":"Model"}},{"description":"Filter to a single endpoint (e.g. '/v1/chat/completions')","in":"query","name":"endpoint","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Filter to a single endpoint (e.g. '/v1/chat/completions')","title":"Endpoint"}},{"description":"Filter to a single provider (e.g. 'openai')","in":"query","name":"provider","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Filter to a single provider (e.g. 'openai')","title":"Provider"}},{"description":"Filter to a single provenance source (e.g. 'gateway' or 'claude_code')","in":"query","name":"source","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Filter to a single provenance source (e.g. 'gateway' or 'claude_code')","title":"Source"}},{"description":"Filter to a single session (source_label): the session's trace id on a gateway request that named one, or the session/project label imported usage carried","in":"query","name":"source_label","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Filter to a single session (source_label): the session's trace id on a gateway request that named one, or the session/project label imported usage carried","title":"Source Label"}},{"description":"Filter by a request tag (what a request sent in its `metadata`), as `key:value`. Repeatable: values for the same key match any of them, and different keys must all match. At most 50 per call.","in":"query","name":"tag","required":false,"schema":{"anyOf":[{"items":{"type":"string"},"maxItems":50,"type":"array"},{"type":"null"}],"description":"Filter by a request tag (what a request sent in its `metadata`), as `key:value`. Repeatable: values for the same key match any of them, and different keys must all match. At most 50 per call.","title":"Tag"}},{"description":"Filter to one or more API key ids; repeatable (api_key_id=a&api_key_id=b). Several values match any of them. At most 50 per call.","in":"query","name":"api_key_id","required":false,"schema":{"anyOf":[{"items":{"type":"string"},"maxItems":50,"type":"array"},{"type":"null"}],"description":"Filter to one or more API key ids; repeatable (api_key_id=a&api_key_id=b). Several values match any of them. At most 50 per call.","title":"Api Key Id"}},{"description":"Filter by token-pricing state: true = only rows whose model tokens were priced, false = only rows that still need pricing (no cost at all, or tokens that were never metered because the model had no rate). A row charged only for gateway-run tool calls still counts as needing pricing.","in":"query","name":"priced","required":false,"schema":{"anyOf":[{"type":"boolean"},{"type":"null"}],"description":"Filter by token-pricing state: true = only rows whose model tokens were priced, false = only rows that still need pricing (no cost at all, or tokens that were never metered because the model had no rate). A row charged only for gateway-run tool calls still counts as needing pricing.","title":"Priced"}},{"description":"Filter to requests that ran a gateway-run tool. 'any' matches any tool; a tool name (web_search, web_fetch, code_execution) matches that tool specifically.","in":"query","name":"tool","required":false,"schema":{"anyOf":[{"enum":["any","web_search","web_fetch","code_execution"],"type":"string"},{"type":"null"}],"description":"Filter to requests that ran a gateway-run tool. 'any' matches any tool; a tool name (web_search, web_fetch, code_execution) matches that tool specifically.","title":"Tool"}},{"description":"Filter by budget participation, which is not the same question as provenance: true = only enforced gateway rows, false = every row that never touches a budget, meaning imported usage and also gateway traffic on a budget-exempt key","in":"query","name":"counts_toward_budget","required":false,"schema":{"anyOf":[{"type":"boolean"},{"type":"null"}],"description":"Filter by budget participation, which is not the same question as provenance: true = only enforced gateway rows, false = every row that never touches a budget, meaning imported usage and also gateway traffic on a budget-exempt key","title":"Counts Toward Budget"}},{"description":"Only usage recorded in this workspace.","in":"query","name":"workspace_id","required":false,"schema":{"anyOf":[{"format":"uuid","type":"string"},{"type":"null"}],"description":"Only usage recorded in this workspace.","title":"Workspace Id"}},{"description":"Time-series granularity: 'hour' or 'day'","in":"query","name":"bucket","required":false,"schema":{"default":"day","description":"Time-series granularity: 'hour' or 'day'","enum":["hour","day"],"title":"Bucket","type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UsageGroupedSeries"}}},"description":"Successful Response"},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}},"description":"Validation Error"}},"summary":"Usage Series","tags":["usage"]}}}}
```

## Set Usage Price Rows

> Set the cost of imported usage rows from manual per-1M rates.\
> \
> Target either the current selection (\`\`ids\`\`) or everything matching a filter\
> (\`\`by\_filter: true\`\`). Cost / billing meters / pricing breakdown are recomputed\
> from each row's own token counts at the supplied \`\`input\`\` / \`\`output\`\` /\
> \`\`cache\_read\`\` / \`\`cache\_write\`\` per-1M rates (manual rates, not a recompute from\
> configured pricing). Only imported rows (\`\`counts\_toward\_budget = false\`\`) are\
> touched, so \`\`users.spend\`\` is never affected. Master-key only.

```json
{"openapi":"3.1.0","info":{"title":"otari","version":"0.33.0"},"security":[{"ApiKeyAuth":[]},{"XApiKeyAuth":[]}],"components":{"securitySchemes":{"ApiKeyAuth":{"description":"Enter your API key here (sent as Otari-Key header).","in":"header","name":"Otari-Key","type":"apiKey"},"XApiKeyAuth":{"description":"Anthropic-native clients send credentials here (no Bearer prefix).","in":"header","name":"x-api-key","type":"apiKey"}},"schemas":{"UsageSetPriceRequest":{"description":"Selection of imported usage rows plus the manual per-1M rates to price them at.\n\n``input`` and ``output`` are required (every row is charged for them); the cache\nrates are optional and, when omitted, those tokens fold into the fresh-input charge\nexactly as an unpriced cache rate does in normal metered pricing.","properties":{"api_key_id":{"anyOf":[{"type":"string"},{"items":{"type":"string"},"maxItems":50,"type":"array"},{"type":"null"}],"title":"Api Key Id"},"by_filter":{"default":false,"title":"By Filter","type":"boolean"},"cache_read_price_per_million":{"anyOf":[{"minimum":0,"type":"number"},{"type":"null"}],"title":"Cache Read Price Per Million"},"cache_write_price_per_million":{"anyOf":[{"minimum":0,"type":"number"},{"type":"null"}],"title":"Cache Write Price Per Million"},"end_date":{"anyOf":[{"format":"date-time","type":"string"},{"type":"null"}],"title":"End Date"},"endpoint":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Endpoint"},"ids":{"anyOf":[{"items":{"type":"string"},"maxItems":1000,"type":"array"},{"type":"null"}],"title":"Ids"},"input_price_per_million":{"minimum":0,"title":"Input Price Per Million","type":"number"},"model":{"anyOf":[{"type":"string"},{"items":{"type":"string"},"maxItems":50,"type":"array"},{"type":"null"}],"title":"Model"},"output_price_per_million":{"minimum":0,"title":"Output Price Per Million","type":"number"},"priced":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Priced"},"provider":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Provider"},"source":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Source"},"source_label":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Source Label"},"start_date":{"anyOf":[{"format":"date-time","type":"string"},{"type":"null"}],"title":"Start Date"},"status":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Status"},"tool":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Tool"},"user_id":{"anyOf":[{"type":"string"},{"items":{"type":"string"},"maxItems":50,"type":"array"},{"type":"null"}],"title":"User Id"},"workspace_id":{"anyOf":[{"format":"uuid","type":"string"},{"type":"null"}],"title":"Workspace Id"}},"required":["input_price_per_million","output_price_per_million"],"title":"UsageSetPriceRequest","type":"object"},"UsageSetPriceResult":{"description":"Outcome of a manual repricing pass.\n\n``matched`` imported rows were recomputed; ``updated`` had their stored cost\nchanged and ``unchanged`` already matched the recomputed value.","properties":{"matched":{"default":0,"title":"Matched","type":"integer"},"unchanged":{"default":0,"title":"Unchanged","type":"integer"},"updated":{"default":0,"title":"Updated","type":"integer"}},"title":"UsageSetPriceResult","type":"object"},"HTTPValidationError":{"properties":{"code":{"const":"invalid_request","description":"Stable error code, also sent as the Otari-Error-Code header.","title":"Code","type":"string"},"detail":{"items":{"$ref":"#/components/schemas/ValidationError"},"title":"Detail","type":"array"}},"title":"HTTPValidationError","type":"object"},"ValidationError":{"properties":{"ctx":{"title":"Context","type":"object"},"input":{"title":"Input"},"loc":{"items":{"anyOf":[{"type":"string"},{"type":"integer"}]},"title":"Location","type":"array"},"msg":{"title":"Message","type":"string"},"type":{"title":"Error Type","type":"string"}},"required":["loc","msg","type"],"title":"ValidationError","type":"object"}}},"paths":{"/api/v1/usage/set-price":{"post":{"description":"Set the cost of imported usage rows from manual per-1M rates.\n\nTarget either the current selection (``ids``) or everything matching a filter\n(``by_filter: true``). Cost / billing meters / pricing breakdown are recomputed\nfrom each row's own token counts at the supplied ``input`` / ``output`` /\n``cache_read`` / ``cache_write`` per-1M rates (manual rates, not a recompute from\nconfigured pricing). Only imported rows (``counts_toward_budget = false``) are\ntouched, so ``users.spend`` is never affected. Master-key only.","operationId":"usage-set_usage_price_rows","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UsageSetPriceRequest"}}},"required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UsageSetPriceResult"}}},"description":"Successful Response"},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}},"description":"Validation Error"}},"summary":"Set Usage Price Rows","tags":["usage"]}}}}
```

## Usage Summary

> Aggregate spend, tokens, and request volume for the dashboard Usage page.\
> \
> Range-bounded (default last 30 days, hard-capped): unlike the raw \`\`/api/v1/usage\`\`\
> list, every aggregate is scoped to a bounded window so it stays served by the\
> timestamp index. Returns grand totals, breakdowns by model / user / API key /\
> source / session (\`\`source\_label\`\`) / endpoint / provider (top rows plus a\
> reconciling \`\`other\`\` fold, billed token counts), the error taxonomy grouped\
> by failure status code, and a UTC-bucketed time series carrying each bucket's\
> error count and billed token composition (input incl. cache, cache read/write,\
> output).\
> \
> Each breakdown is its own \`\`GROUP BY\`\` pass, so a caller that reads only the\
> totals or the series should narrow \`\`dimensions\`\` rather than pay for all eight\
> (the dashboard's tiles, timeline context, and model typeahead all do). Omitting\
> the parameter keeps the full set.\
> \
> \`\`model\`\`, \`\`user\_id\`\`, and \`\`api\_key\_id\`\` are repeatable: several values match\
> any of them, so one chart can compare a handful of models, users, or keys.

```json
{"openapi":"3.1.0","info":{"title":"otari","version":"0.33.0"},"security":[{"ApiKeyAuth":[]},{"XApiKeyAuth":[]}],"components":{"securitySchemes":{"ApiKeyAuth":{"description":"Enter your API key here (sent as Otari-Key header).","in":"header","name":"Otari-Key","type":"apiKey"},"XApiKeyAuth":{"description":"Anthropic-native clients send credentials here (no Bearer prefix).","in":"header","name":"x-api-key","type":"apiKey"}},"schemas":{"UsageSummary":{"description":"Aggregate spend/volume for the Usage & analytics page.\n\nEvery breakdown field is always present. One the caller excluded through\n``dimensions`` comes back as an empty list, the same shape a window with no\nmatching rows produces, so narrowing the selector never changes the schema.","properties":{"bucket":{"enum":["hour","day"],"title":"Bucket","type":"string"},"by_api_key":{"items":{"$ref":"#/components/schemas/UsageGroupRow"},"title":"By Api Key","type":"array"},"by_endpoint":{"items":{"$ref":"#/components/schemas/UsageGroupRow"},"title":"By Endpoint","type":"array"},"by_model":{"items":{"$ref":"#/components/schemas/UsageGroupRow"},"title":"By Model","type":"array"},"by_provider":{"items":{"$ref":"#/components/schemas/UsageGroupRow"},"title":"By Provider","type":"array"},"by_source":{"items":{"$ref":"#/components/schemas/UsageGroupRow"},"title":"By Source","type":"array"},"by_source_label":{"items":{"$ref":"#/components/schemas/UsageGroupRow"},"title":"By Source Label","type":"array"},"by_tag":{"default":[],"items":{"$ref":"#/components/schemas/UsageGroupRow"},"title":"By Tag","type":"array"},"by_tool":{"default":[],"items":{"$ref":"#/components/schemas/UsageToolRow"},"title":"By Tool","type":"array"},"by_user":{"items":{"$ref":"#/components/schemas/UsageGroupRow"},"title":"By User","type":"array"},"end_date":{"title":"End Date","type":"string"},"errors_by_status_code":{"items":{"$ref":"#/components/schemas/UsageErrorCodeRow"},"title":"Errors By Status Code","type":"array"},"series":{"items":{"$ref":"#/components/schemas/UsageSeriesPoint"},"title":"Series","type":"array"},"start_date":{"title":"Start Date","type":"string"},"totals":{"$ref":"#/components/schemas/UsageTotals"}},"required":["start_date","end_date","bucket","totals","by_model","by_user","by_api_key","by_source","by_source_label","by_endpoint","by_provider","errors_by_status_code","series"],"title":"UsageSummary","type":"object"},"UsageGroupRow":{"description":"One breakdown row (a model, a user, an API key, a session, ...).\n\n``key`` is None both for the synthesized fold row (``is_other=True``) and for a\nreal group whose column was NULL (e.g. usage from a since-deleted user, with\n``is_other=False``). ``is_other`` disambiguates the two so the UI does not\nmislabel deleted-user usage as the fold.","properties":{"cost":{"title":"Cost","type":"number"},"is_other":{"default":false,"title":"Is Other","type":"boolean"},"key":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Key"},"label":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Label"},"requests":{"title":"Requests","type":"integer"},"tokens":{"title":"Tokens","type":"integer"}},"required":["key","cost","tokens","requests"],"title":"UsageGroupRow","type":"object"},"UsageToolRow":{"description":"Spend and volume for one gateway-run tool inside the window.\n\n``calls`` counts billable calls, not requests: one request can run a tool\nseveral times, which is the whole reason a per-tool view exists. ``errors``\ncounts calls that failed and were therefore never billed. ``requests`` is how\nmany requests ran the tool at least once, so the number reconciles with the\nActivity list when the same filter is applied there.","properties":{"calls":{"title":"Calls","type":"integer"},"cost":{"title":"Cost","type":"number"},"errors":{"title":"Errors","type":"integer"},"requests":{"title":"Requests","type":"integer"},"tool":{"title":"Tool","type":"string"}},"required":["tool","calls","errors","requests","cost"],"title":"UsageToolRow","type":"object"},"UsageErrorCodeRow":{"description":"One error-taxonomy row: the failures in the window sharing a status code.\n\n``status_code`` is None for failures recorded without one (rows written\nbefore the column existed, and failures no HTTP status describes, e.g. a\nstream that finished without usage data under the ``fail`` policy).\n``error_class`` is the coarse display bucket derived from the code, so a UI\ncan group \"provider fault\" against \"my own misconfiguration\" without\nre-deriving a status ladder; the raw code stays alongside it for precision.","properties":{"error_class":{"enum":["pricing","rate_limit","auth","provider_error","client_error","unknown"],"title":"Error Class","type":"string"},"requests":{"title":"Requests","type":"integer"},"status_code":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Status Code"}},"required":["status_code","error_class","requests"],"title":"UsageErrorCodeRow","type":"object"},"UsageSeriesPoint":{"description":"One time bucket. ``bucket_start`` is canonical ISO-8601 UTC (``...Z``),\nidentical across SQLite and PostgreSQL for the same underlying instant.\n\n``tokens`` stays the raw provider-reported total (the field predates the\ncomposition split and external consumers may read it). The billed\ncomposition fields are normalized via billing meters (see ``_billed_expr``):\n``input_tokens`` includes both cache buckets, so a chart derives fresh input\nas ``max(0, input_tokens - cache_read_tokens - cache_write_tokens)`` and the\nbilled total as ``fresh + cache_read + cache_write + output``.","properties":{"bucket_start":{"title":"Bucket Start","type":"string"},"cache_read_tokens":{"default":0,"title":"Cache Read Tokens","type":"integer"},"cache_write_tokens":{"default":0,"title":"Cache Write Tokens","type":"integer"},"cost":{"title":"Cost","type":"number"},"errors":{"default":0,"title":"Errors","type":"integer"},"input_tokens":{"default":0,"title":"Input Tokens","type":"integer"},"output_tokens":{"default":0,"title":"Output Tokens","type":"integer"},"requests":{"title":"Requests","type":"integer"},"tokens":{"title":"Tokens","type":"integer"}},"required":["bucket_start","cost","tokens","requests"],"title":"UsageSeriesPoint","type":"object"},"UsageTotals":{"description":"Grand totals over the filtered window.","properties":{"avg_latency_ms":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Avg Latency Ms"},"billed_input_tokens":{"default":0,"title":"Billed Input Tokens","type":"integer"},"billed_output_tokens":{"default":0,"title":"Billed Output Tokens","type":"integer"},"cache_read_tokens":{"title":"Cache Read Tokens","type":"integer"},"cache_write_1h_tokens":{"title":"Cache Write 1H Tokens","type":"integer"},"cache_write_tokens":{"title":"Cache Write Tokens","type":"integer"},"completion_tokens":{"title":"Completion Tokens","type":"integer"},"cost":{"title":"Cost","type":"number"},"error_count":{"title":"Error Count","type":"integer"},"prompt_tokens":{"title":"Prompt Tokens","type":"integer"},"reasoning_tokens":{"default":0,"title":"Reasoning Tokens","type":"integer"},"request_count":{"title":"Request Count","type":"integer"},"total_tokens":{"title":"Total Tokens","type":"integer"},"unpriced_requests":{"default":0,"title":"Unpriced Requests","type":"integer"}},"required":["cost","prompt_tokens","completion_tokens","total_tokens","cache_read_tokens","cache_write_tokens","cache_write_1h_tokens","request_count","error_count","avg_latency_ms"],"title":"UsageTotals","type":"object"},"HTTPValidationError":{"properties":{"code":{"const":"invalid_request","description":"Stable error code, also sent as the Otari-Error-Code header.","title":"Code","type":"string"},"detail":{"items":{"$ref":"#/components/schemas/ValidationError"},"title":"Detail","type":"array"}},"title":"HTTPValidationError","type":"object"},"ValidationError":{"properties":{"ctx":{"title":"Context","type":"object"},"input":{"title":"Input"},"loc":{"items":{"anyOf":[{"type":"string"},{"type":"integer"}]},"title":"Location","type":"array"},"msg":{"title":"Message","type":"string"},"type":{"title":"Error Type","type":"string"}},"required":["loc","msg","type"],"title":"ValidationError","type":"object"}}},"paths":{"/api/v1/usage/summary":{"get":{"description":"Aggregate spend, tokens, and request volume for the dashboard Usage page.\n\nRange-bounded (default last 30 days, hard-capped): unlike the raw ``/api/v1/usage``\nlist, every aggregate is scoped to a bounded window so it stays served by the\ntimestamp index. Returns grand totals, breakdowns by model / user / API key /\nsource / session (``source_label``) / endpoint / provider (top rows plus a\nreconciling ``other`` fold, billed token counts), the error taxonomy grouped\nby failure status code, and a UTC-bucketed time series carrying each bucket's\nerror count and billed token composition (input incl. cache, cache read/write,\noutput).\n\nEach breakdown is its own ``GROUP BY`` pass, so a caller that reads only the\ntotals or the series should narrow ``dimensions`` rather than pay for all eight\n(the dashboard's tiles, timeline context, and model typeahead all do). Omitting\nthe parameter keeps the full set.\n\n``model``, ``user_id``, and ``api_key_id`` are repeatable: several values match\nany of them, so one chart can compare a handful of models, users, or keys.","operationId":"usage-usage_summary","parameters":[{"description":"Return logs with timestamp >= start_date (ISO 8601 or Unix epoch seconds)","in":"query","name":"start_date","required":false,"schema":{"anyOf":[{"format":"date-time","type":"string"},{"type":"null"}],"description":"Return logs with timestamp >= start_date (ISO 8601 or Unix epoch seconds)","title":"Start Date"}},{"description":"Return logs with timestamp < end_date (ISO 8601 or Unix epoch seconds)","in":"query","name":"end_date","required":false,"schema":{"anyOf":[{"format":"date-time","type":"string"},{"type":"null"}],"description":"Return logs with timestamp < end_date (ISO 8601 or Unix epoch seconds)","title":"End Date"}},{"description":"Filter to one or more users; repeatable (user_id=a&user_id=b). Several values match any of them. At most 50 per call.","in":"query","name":"user_id","required":false,"schema":{"anyOf":[{"items":{"type":"string"},"maxItems":50,"type":"array"},{"type":"null"}],"description":"Filter to one or more users; repeatable (user_id=a&user_id=b). Several values match any of them. At most 50 per call.","title":"User Id"}},{"description":"Filter to a single status: 'success', 'error', or 'absorbed' (an attempt a routing policy recovered from, excluded from error_count and request_count)","in":"query","name":"status","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Filter to a single status: 'success', 'error', or 'absorbed' (an attempt a routing policy recovered from, excluded from error_count and request_count)","title":"Status"}},{"description":"Filter to a single failure status code (e.g. 429 for provider rate limits, 402 for missing-pricing rejections). Only error rows carry one, so this filter also restricts to status='error' unless 'status' is given explicitly","in":"query","name":"status_code","required":false,"schema":{"anyOf":[{"type":"integer"},{"type":"null"}],"description":"Filter to a single failure status code (e.g. 429 for provider rate limits, 402 for missing-pricing rejections). Only error rows carry one, so this filter also restricts to status='error' unless 'status' is given explicitly","title":"Status Code"}},{"description":"Filter to one or more models; repeatable (model=a&model=b). Several values match any of them. At most 50 per call.","in":"query","name":"model","required":false,"schema":{"anyOf":[{"items":{"type":"string"},"maxItems":50,"type":"array"},{"type":"null"}],"description":"Filter to one or more models; repeatable (model=a&model=b). Several values match any of them. At most 50 per call.","title":"Model"}},{"description":"Filter to a single endpoint (e.g. '/v1/chat/completions')","in":"query","name":"endpoint","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Filter to a single endpoint (e.g. '/v1/chat/completions')","title":"Endpoint"}},{"description":"Filter to a single provider (e.g. 'openai')","in":"query","name":"provider","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Filter to a single provider (e.g. 'openai')","title":"Provider"}},{"description":"Filter to a single provenance source (e.g. 'gateway' or 'claude_code')","in":"query","name":"source","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Filter to a single provenance source (e.g. 'gateway' or 'claude_code')","title":"Source"}},{"description":"Filter to a single session (source_label): the session's trace id on a gateway request that named one, or the session/project label imported usage carried","in":"query","name":"source_label","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Filter to a single session (source_label): the session's trace id on a gateway request that named one, or the session/project label imported usage carried","title":"Source Label"}},{"description":"Filter by a request tag (what a request sent in its `metadata`), as `key:value`. Repeatable: values for the same key match any of them, and different keys must all match. At most 50 per call.","in":"query","name":"tag","required":false,"schema":{"anyOf":[{"items":{"type":"string"},"maxItems":50,"type":"array"},{"type":"null"}],"description":"Filter by a request tag (what a request sent in its `metadata`), as `key:value`. Repeatable: values for the same key match any of them, and different keys must all match. At most 50 per call.","title":"Tag"}},{"description":"Filter to one or more API key ids; repeatable (api_key_id=a&api_key_id=b). Several values match any of them. At most 50 per call.","in":"query","name":"api_key_id","required":false,"schema":{"anyOf":[{"items":{"type":"string"},"maxItems":50,"type":"array"},{"type":"null"}],"description":"Filter to one or more API key ids; repeatable (api_key_id=a&api_key_id=b). Several values match any of them. At most 50 per call.","title":"Api Key Id"}},{"description":"Filter by token-pricing state: true = only rows whose model tokens were priced, false = only rows that still need pricing (no cost at all, or tokens that were never metered because the model had no rate). A row charged only for gateway-run tool calls still counts as needing pricing.","in":"query","name":"priced","required":false,"schema":{"anyOf":[{"type":"boolean"},{"type":"null"}],"description":"Filter by token-pricing state: true = only rows whose model tokens were priced, false = only rows that still need pricing (no cost at all, or tokens that were never metered because the model had no rate). A row charged only for gateway-run tool calls still counts as needing pricing.","title":"Priced"}},{"description":"Filter to requests that ran a gateway-run tool. 'any' matches any tool; a tool name (web_search, web_fetch, code_execution) matches that tool specifically.","in":"query","name":"tool","required":false,"schema":{"anyOf":[{"enum":["any","web_search","web_fetch","code_execution"],"type":"string"},{"type":"null"}],"description":"Filter to requests that ran a gateway-run tool. 'any' matches any tool; a tool name (web_search, web_fetch, code_execution) matches that tool specifically.","title":"Tool"}},{"description":"Filter by budget participation, which is not the same question as provenance: true = only enforced gateway rows, false = every row that never touches a budget, meaning imported usage and also gateway traffic on a budget-exempt key","in":"query","name":"counts_toward_budget","required":false,"schema":{"anyOf":[{"type":"boolean"},{"type":"null"}],"description":"Filter by budget participation, which is not the same question as provenance: true = only enforced gateway rows, false = every row that never touches a budget, meaning imported usage and also gateway traffic on a budget-exempt key","title":"Counts Toward Budget"}},{"description":"Only usage recorded in this workspace.","in":"query","name":"workspace_id","required":false,"schema":{"anyOf":[{"format":"uuid","type":"string"},{"type":"null"}],"description":"Only usage recorded in this workspace.","title":"Workspace Id"}},{"description":"Time-series granularity: 'hour' or 'day'","in":"query","name":"bucket","required":false,"schema":{"default":"day","description":"Time-series granularity: 'hour' or 'day'","enum":["hour","day"],"title":"Bucket","type":"string"}},{"description":"Which breakdowns to compute; repeatable (dimensions=model&dimensions=user). Each value names the 'by_<value>' response field it fills, except 'status_code', which fills the failure taxonomy in 'errors_by_status_code'. Omit for every breakdown (the default); pass 'none' for a totals-and-series-only response. Each dimension left out skips one GROUP BY scan, so a caller that reads only the tiles or the time series should say so. Fields that were not requested come back empty.","in":"query","name":"dimensions","required":false,"schema":{"anyOf":[{"items":{"enum":["model","user","api_key","source","source_label","endpoint","provider","status_code","tool","none"],"type":"string"},"type":"array"},{"type":"null"}],"description":"Which breakdowns to compute; repeatable (dimensions=model&dimensions=user). Each value names the 'by_<value>' response field it fills, except 'status_code', which fills the failure taxonomy in 'errors_by_status_code'. Omit for every breakdown (the default); pass 'none' for a totals-and-series-only response. Each dimension left out skips one GROUP BY scan, so a caller that reads only the tiles or the time series should say so. Fields that were not requested come back empty.","title":"Dimensions"}},{"description":"A tag key to break spend down by, returned as `by_tag`. Rows that do not carry the tag group under a null key.","in":"query","name":"group_by_tag","required":false,"schema":{"anyOf":[{"maxLength":64,"minLength":1,"type":"string"},{"type":"null"}],"description":"A tag key to break spend down by, returned as `by_tag`. Rows that do not carry the tag group under a null key.","title":"Group By Tag"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UsageSummary"}}},"description":"Successful Response"},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}},"description":"Validation Error"}},"summary":"Usage Summary","tags":["usage"]}}}}
```


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation by asking a question.

Perform an HTTP GET request on the following URL with the `ask` and `goal` query parameters:

```
GET https://docs.otari.ai/otari/for-integrators/api-reference/usage.md?ask=<question>&goal=<user_goal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is what the user is ultimately trying to achieve, the reason they need the answer. Sharing it helps GitBook give you a better, more relevant answer. A goal is most helpful when it describes the outcome the user wants rather than restating the question. For example, with `ask=how do I create an API token`, a goal like `automate deployments from our CI pipeline` lets GitBook tailor the answer to that use case.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
