> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://docs.v2.rasen.ai/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.v2.rasen.ai/_mcp/server.

# Query usage with a JSON filter

POST https://api.v2.rasen.ai/v1/usage/query
Content-Type: application/json

Bounded typed alternative to GET /usage for large agent or call selections, with explicit agent and call grouping. POST is used only because the selection can exceed URL limits: this is a read with no effect, safe to retry, needs no Idempotency-Key and counts toward read rate limits. Same workspace authorization, result rows, ordering and cursor rules as the GET form. Body and filter limits are separate from GET: up to 1000 ID values in total and 128 KiB encoded. Results stay paged (limit at most 200); nothing expands transcripts, results or variables. Not a general query language.

Reference: https://docs.v2.rasen.ai/api-reference/usage/query

## Authentication

- `Authorization` header (bearer token, required) — Create and manage workspace-owned keys through the existing app session routes, outside this public API. Creator and manager identities are audit actors only. Key validity follows workspace policy, expiry and explicit revocation, independently of creator membership. Do not include a workspace-selection header.

## Request

### Body (application/json)

This endpoint expects an UsageQueryRequest.

- `filter` (UsageQueryFilter, required) — Same field names and meaning as the GET /usage query parameters. UTC [start,end), maximum 93 days, default trailing 30 days. Facts are filtered before aggregation.
- `group_by` (list of enum, optional) — Same values as the GET group_by filter. [] or omission combines matching facts by component and unit. agent adds agent_id; call adds call_id. No other dimensions.
  - Allowed values: `agent`, `call`
- `limit` (integer, optional, default: 50) — Page size, same bounds as GET.
- `cursor` (string, optional) — Opaque cursor from the previous page. It binds the filter and limit: send them unchanged with it; a change is refused with invalid_request. Expires after 24 hours.

## Response

### 200

Query usage with a JSON filter.

- `state` (string, required) — Open response vocabulary. Preserve unknown values; never assume they mean success.
- `items` (list of UsageItem, required)
- `next_cursor` (string, required, nullable)
- `range` (UsagePageRange, required) — The resolved UTC [start, end) range used, including defaults (trailing 30 days ending now).

## Errors

### 400 Bad Request Error

Invalid input or cursor. Correct the request.

- `error` (ErrorResponseError, required) — Only code and message are required. Rasen sends details, request_id and retry on every error, but clients must parse an error that lacks them, so a typed error is never lost to a parsing failure.

### 401 Unauthorized Error

Invalid, expired or revoked API key. Creator departure or membership changes do not invalidate a workspace key.

- `error` (ErrorResponseError, required) — Only code and message are required. Rasen sends details, request_id and retry on every error, but clients must parse an error that lacks them, so a typed error is never lost to a parsing failure.

### 403 Forbidden Error

Operation not allowed by current credential or workspace access.

- `error` (ErrorResponseError, required) — Only code and message are required. Rasen sends details, request_id and retry on every error, but clients must parse an error that lacks them, so a typed error is never lost to a parsing failure.

### 413 Content Too Large Error

Body or upload exceeds its allowed limit.

- `error` (ErrorResponseError, required) — Only code and message are required. Rasen sends details, request_id and retry on every error, but clients must parse an error that lacks them, so a typed error is never lost to a parsing failure.

### 429 Too Many Requests Error

Rate limited. Respect Retry-After and retry the same intent where permitted.

- `error` (ErrorResponseError, required) — Only code and message are required. Rasen sends details, request_id and retry on every error, but clients must parse an error that lacks them, so a typed error is never lost to a parsing failure.

### 500 Internal Server Error

Internal error. The effect may have happened. Retry the same request with the same Idempotency-Key; read the resource before creating a new intent. For an operation that takes an Idempotency-Key, a client retry with the same key is safe: the server replays the stored outcome if the first attempt took effect; if the first attempt had no effect, the retry performs the operation once. Never retry with a new key.

- `error` (ErrorResponseError, required) — Only code and message are required. Rasen sends details, request_id and retry on every error, but clients must parse an error that lacks them, so a typed error is never lost to a parsing failure.

### 503 Service Unavailable Error

Service unavailable. Respect Retry-After where present. For an operation that takes an Idempotency-Key, a client retry with the same key is safe: the server replays the stored outcome if the first attempt took effect; if the first attempt had no effect, the retry performs the operation once. Never retry with a new key.

- `error` (ErrorResponseError, required) — Only code and message are required. Rasen sends details, request_id and retry on every error, but clients must parse an error that lacks them, so a typed error is never lost to a parsing failure.

## Types

### UsageQueryFilter

Same field names and meaning as the GET /usage query parameters. UTC [start,end), maximum 93 days, default trailing 30 days. Facts are filtered before aggregation.

- `agent_id` (list of string, optional) — Agent IDs. OR within this field. At most 1000 values across all ID fields of this body. Explicit empty arrays are refused, never treated as no filter. Unknown or inaccessible IDs yield no rows.
- `call_id` (list of string, optional) — Call IDs. OR within this field. At most 1000 values across all ID fields of this body. Explicit empty arrays are refused, never treated as no filter. Unknown or inaccessible IDs yield no rows.
- `component` (list of string, optional) — Metering components, as for GET /usage. OR within this field. Unknown values are refused, not ignored. Empty arrays are refused.
- `start` (datetime, optional)
- `end` (datetime, optional)

### UsageItem

One row per group (component, unit and any requested grouping) for the whole resolved range. Different units never sum. A granularity option may be added later as an optional input.

- `component` (string, required) — Open component vocabulary, for example telephony or stt.
- `unit` (string, required) — Open unit vocabulary; never infer a price.
- `quantity` (double, required)
- `started_at` (datetime, required) — Equals UsagePage.range.start. No time buckets in this version.
- `ended_at` (datetime, required) — Equals UsagePage.range.end.
- `agent_id` (string, required, nullable) — Present agent identity when group_by=agent; null for ungrouped facts or unassigned usage.
- `call_id` (string, required, nullable) — Present when grouped by call; otherwise null.
- `events` (integer, required) — Number of metered facts combined into this row.

### UsagePageRange

The resolved UTC [start, end) range used, including defaults (trailing 30 days ending now).

- `start` (datetime, required)
- `end` (datetime, required)

### ErrorResponseError

Only code and message are required. Rasen sends details, request_id and retry on every error, but clients must parse an error that lacks them, so a typed error is never lost to a parsing failure.

- `code` (string, required) — Stable code. Branch on code (and details.reason for conflict), never on message. Each code has one HTTP status; see components.x-error-catalog.
- `message` (string, required) — Safe description; no provider payload or submitted secrets.
- `details` (map from string to any, optional) — Safe code-specific details. The public fields for each code are listed in components.x-error-catalog. Details never contain a request fingerprint, a hash of a request, secret material or another tenant's data. forbidden may include required_capabilities (a list).
- `request_id` (string, optional) — Same value as the X-Request-Id header. Quote it to support.
- `retry` (string, optional) — Retry class of this code: no (fix the request or state first), yes (retry the same request and the same Idempotency-Key), retry_after (wait for Retry-After, then retry the same request). Open response vocabulary. Preserve unknown values; never assume they mean success.

## Examples

**Request**

```json
{
  "filter": {
    "agent_id": [
      "agent_01j00000000000000000000000"
    ],
    "start": "2026-09-01T00:00:00Z",
    "end": "2026-09-25T00:00:00Z"
  },
  "group_by": [
    "agent"
  ]
}
```

**Response**

```json
{
  "state": "measured",
  "items": [
    {
      "component": "telephony",
      "unit": "seconds",
      "quantity": 5412,
      "started_at": "2026-09-01T00:00:00Z",
      "ended_at": "2026-09-25T00:00:00Z",
      "agent_id": "agent_01j00000000000000000000000",
      "call_id": null,
      "events": 37
    }
  ],
  "next_cursor": null,
  "range": {
    "start": "2026-09-01T00:00:00Z",
    "end": "2026-09-25T00:00:00Z"
  }
}
```

**SDK Code**

```python usage_query_example
import requests

url = "https://api.v2.rasen.ai/v1/usage/query"

payload = {
    "filter": {
        "agent_id": ["agent_01j00000000000000000000000"],
        "start": "2026-09-01T00:00:00Z",
        "end": "2026-09-25T00:00:00Z"
    },
    "group_by": ["agent"]
}
headers = {
    "Authorization": "Bearer <api_key>",
    "Content-Type": "application/json"
}

response = requests.post(url, json=payload, headers=headers)

print(response.json())
```

```javascript usage_query_example
const url = 'https://api.v2.rasen.ai/v1/usage/query';
const options = {
  method: 'POST',
  headers: {Authorization: 'Bearer <api_key>', 'Content-Type': 'application/json'},
  body: '{"filter":{"agent_id":["agent_01j00000000000000000000000"],"start":"2026-09-01T00:00:00Z","end":"2026-09-25T00:00:00Z"},"group_by":["agent"]}'
};

try {
  const response = await fetch(url, options);
  const data = await response.json();
  console.log(data);
} catch (error) {
  console.error(error);
}
```

```ruby usage_query_example
require 'uri'
require 'net/http'

url = URI("https://api.v2.rasen.ai/v1/usage/query")

http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true

request = Net::HTTP::Post.new(url)
request["Authorization"] = 'Bearer <api_key>'
request["Content-Type"] = 'application/json'
request.body = "{\n  \"filter\": {\n    \"agent_id\": [\n      \"agent_01j00000000000000000000000\"\n    ],\n    \"start\": \"2026-09-01T00:00:00Z\",\n    \"end\": \"2026-09-25T00:00:00Z\"\n  },\n  \"group_by\": [\n    \"agent\"\n  ]\n}"

response = http.request(request)
puts response.read_body
```

```java usage_query_example
import com.mashape.unirest.http.HttpResponse;
import com.mashape.unirest.http.Unirest;

HttpResponse<String> response = Unirest.post("https://api.v2.rasen.ai/v1/usage/query")
  .header("Authorization", "Bearer <api_key>")
  .header("Content-Type", "application/json")
  .body("{\n  \"filter\": {\n    \"agent_id\": [\n      \"agent_01j00000000000000000000000\"\n    ],\n    \"start\": \"2026-09-01T00:00:00Z\",\n    \"end\": \"2026-09-25T00:00:00Z\"\n  },\n  \"group_by\": [\n    \"agent\"\n  ]\n}")
  .asString();
```

```php usage_query_example
<?php
require_once('vendor/autoload.php');

$client = new \GuzzleHttp\Client();

$response = $client->request('POST', 'https://api.v2.rasen.ai/v1/usage/query', [
  'body' => '{
  "filter": {
    "agent_id": [
      "agent_01j00000000000000000000000"
    ],
    "start": "2026-09-01T00:00:00Z",
    "end": "2026-09-25T00:00:00Z"
  },
  "group_by": [
    "agent"
  ]
}',
  'headers' => [
    'Authorization' => 'Bearer <api_key>',
    'Content-Type' => 'application/json',
  ],
]);

echo $response->getBody();
```

```csharp usage_query_example
using RestSharp;

var client = new RestClient("https://api.v2.rasen.ai/v1/usage/query");
var request = new RestRequest(Method.POST);
request.AddHeader("Authorization", "Bearer <api_key>");
request.AddHeader("Content-Type", "application/json");
request.AddParameter("application/json", "{\n  \"filter\": {\n    \"agent_id\": [\n      \"agent_01j00000000000000000000000\"\n    ],\n    \"start\": \"2026-09-01T00:00:00Z\",\n    \"end\": \"2026-09-25T00:00:00Z\"\n  },\n  \"group_by\": [\n    \"agent\"\n  ]\n}", ParameterType.RequestBody);
IRestResponse response = client.Execute(request);
```

```swift usage_query_example
import Foundation

let headers = [
  "Authorization": "Bearer <api_key>",
  "Content-Type": "application/json"
]
let parameters = [
  "filter": [
    "agent_id": ["agent_01j00000000000000000000000"],
    "start": "2026-09-01T00:00:00Z",
    "end": "2026-09-25T00:00:00Z"
  ],
  "group_by": ["agent"]
] as [String : Any]

let postData = JSONSerialization.data(withJSONObject: parameters, options: [])

let request = NSMutableURLRequest(url: NSURL(string: "https://api.v2.rasen.ai/v1/usage/query")! as URL,
                                        cachePolicy: .useProtocolCachePolicy,
                                    timeoutInterval: 10.0)
request.httpMethod = "POST"
request.allHTTPHeaderFields = headers
request.httpBody = postData as Data

let session = URLSession.shared
let dataTask = session.dataTask(with: request as URLRequest, completionHandler: { (data, response, error) -> Void in
  if (error != nil) {
    print(error as Any)
  } else {
    let httpResponse = response as? HTTPURLResponse
    print(httpResponse)
  }
})

dataTask.resume()
```