Receipt Query API
The receipt query API is the HTTP surface for reading the kernel's signed tool-call log. It is served by chio trust serve and by the dashboard backend, with multi-filter, cursor-paginated access to every receipt the kernel has written. This page is the endpoint reference: request schema, response schema, auth, pagination, SDK examples in TypeScript and Python, and the rate limits that govern production use.
Looking for the dashboard?
Endpoint Summary
| Method | Path | Purpose |
|---|---|---|
| GET | /v1/receipts/query | Primary receipt query with filters and cursor pagination |
| GET | /v1/agents/{subject_key}/receipts | Agent-scoped convenience endpoint; accepts limit and cursor only |
| GET | /v1/receipts/analytics | Aggregate counters and groupings over the same corpus |
| GET | /v1/reports/operator | Composed operator report (analytics + cost attribution + budget rollups) |
All endpoints require a bearer token in the Authorization header. All filters are combined with AND semantics. Omitting a parameter disables that filter.
Authentication
The trust-control server authenticates API clients with a bearer token. Supply the token in the Authorization header:
curl -sS https://trust.example.com/v1/receipts/query \
-H "Authorization: Bearer my-service-token"Tokens are provisioned out-of-band (typically by the operator who runs chio trust serve). Each token is mapped to a role that gates which receipts it can read. A service token scoped to a single tenant sees only receipts for that tenant. A read-only audit token sees everything but can neither mutate nor export raw evidence.
Treat tokens like keys
Filter Parameters
All parameters are query-string parameters. Names are camelCase on the wire, matching the server's serde(rename_all = "camelCase") struct.
| Parameter | Type | Description |
|---|---|---|
capabilityId | string | Exact match on capability ID |
toolServer | string | Exact match on tool server name (server_id) |
toolName | string | Exact match on tool name |
outcome | string | One of allow, deny, cancelled, incomplete |
since | u64 | Only receipts with timestamp >= since (unix seconds, inclusive) |
until | u64 | Only receipts with timestamp <= until (unix seconds, inclusive) |
minCost | u64 | Minimum cost_charged in minor units. Receipts without financial metadata are excluded when set. |
maxCost | u64 | Maximum cost_charged in minor units. Receipts without financial metadata are excluded when set. |
agentSubject | string | Hex-encoded Ed25519 agent subject key. Resolved from attribution metadata or capability lineage. |
cursor | u64 | Pagination cursor. Returns only receipts with seq > cursor (exclusive). |
limit | usize | Max results per page. Default 50. Server cap is 200; values above the cap are silently reduced. |
Some SDKs expose a higher logical cap
Response Shape
{
"totalCount": 1024,
"nextCursor": 47,
"receipts": [ /* ChioReceipt objects */ ]
}totalCount is the count of all receipts matching the filters, independent of page limit or cursor. Use it to render "N total" counters in a UI without paginating through every page.
nextCursor is the seq value of the last receipt in the current page. Pass it as cursor on the next request to fetch the following page. When nextCursor is null (or absent), the current page is the last one.
receipts is an array of ChioReceipt objects ordered by seq ascending. Each receipt is the exact, signature-covered payload that chio wrote; you can verify the signature client-side using the kernel's published key.
Cursor-Based Pagination
The cursor is the seq column value from the last receipt in a page. Pagination is forward-only and append-safe: as new receipts land, they appear on future pages without disturbing the current page's boundaries.
# Page 1
GET /v1/receipts/query?toolServer=shell&limit=50
# Response includes nextCursor: 147
# Page 2
GET /v1/receipts/query?toolServer=shell&limit=50&cursor=147
# When nextCursor is absent or null, the page is the last one.The cursor is a raw seq integer (u64). Treat the value as opaque in your own code: round-trip what the server hands back in nextCursor rather than parsing or constructing cursors yourself.
Example Request and Response
GET /v1/receipts/query?outcome=deny&since=1700000000&limit=2
Authorization: Bearer my-service-token{
"totalCount": 8,
"nextCursor": 23,
"receipts": [
{
"id": "receipt-001",
"timestamp": 1700000100,
"capability_id": "cap-abc",
"tool_server": "filesystem",
"tool_name": "write_file",
"decision": {
"verdict": "deny",
"reason": "path outside allowed prefix",
"guard": "path_allowlist"
},
"content_hash": "...",
"policy_hash": "...",
"evidence": [],
"signature": "..."
},
{
"id": "receipt-002",
"timestamp": 1700000250,
"capability_id": "cap-abc",
"tool_server": "shell",
"tool_name": "exec",
"decision": {
"verdict": "deny",
"reason": "budget exhausted",
"guard": "monetary_budget"
},
"metadata": {
"attribution": {
"subject_key": "ed25519-subject-hex",
"issuer_key": "ed25519-issuer-hex",
"delegation_depth": 0,
"grant_index": 0
},
"financial": {
"grant_index": 0,
"cost_charged": 0,
"currency": "USD",
"budget_remaining": 0,
"budget_total": 10000,
"delegation_depth": 0,
"root_budget_holder": "agent-root",
"settlement_status": "not_applicable",
"attempted_cost": 500
}
},
"content_hash": "...",
"policy_hash": "...",
"evidence": [],
"signature": "..."
}
]
}Agent-Scoped Endpoint
A shorter URL is available for per-agent lookups:
GET /v1/agents/{subject_key}/receipts?limit=50&cursor=0
Authorization: Bearer my-service-tokenThis is equivalent to calling /v1/receipts/query?agentSubject={subject_key} but only accepts limit and cursor query parameters. Use it when you want a clean "show me this agent" URL for auditors or when your routing layer makes per-agent path-scoping easier to reason about than query-string filtering.
TypeScript SDK
The @chio-protocol/sdk package ships a ReceiptQueryClient that wraps this endpoint. It exposes a single-page list() call and a stream() async iterator that paginates automatically.
import { ReceiptQueryClient } from "@chio-protocol/sdk";
const client = new ReceiptQueryClient({
baseUrl: "https://trust.example.com",
bearerToken: process.env.CHIO_CONTROL_TOKEN!,
});
// Single page.
const page = await client.list({
outcome: "deny",
since: 1700000000,
limit: 50,
});
console.log(`${page.totalCount} total denies`);
for (const receipt of page.receipts) {
console.log(receipt.id, receipt.decision);
}
// Auto-paginating async iterator.
for await (const receipt of client.stream({
toolServer: "payment-server",
since: 1700000000,
})) {
if (receipt.decision.verdict === "deny") {
console.warn(`denied: ${receipt.decision.reason}`);
}
}Python SDK
import os
from chio import ReceiptQueryClient
client = ReceiptQueryClient(
base_url="https://trust.example.com",
bearer_token=os.environ["CHIO_CONTROL_TOKEN"],
)
# Single page.
page = await client.list(outcome="deny", since=1700000000, limit=50)
print(f"{page.total_count} total denies")
for receipt in page.receipts:
print(receipt.id, receipt.decision)
# Auto-paginating async iterator.
async for receipt in client.stream(
tool_server="payment-server",
since=1700000000,
):
if receipt.decision["verdict"] == "deny":
print(f"denied: {receipt.decision['reason']}")Both clients handle bearer-token refresh, server-side retries on transient errors, and cursor threading. The streaming iterators stop when the server returns an empty page or a nextCursor of null.
End-to-End Pagination Example
If you need to implement pagination manually (or in a language without an SDK), the loop is short:
cursor=""
while : ; do
resp=$(curl -sS \
"https://trust.example.com/v1/receipts/query?outcome=deny&limit=200${cursor:+&cursor=$cursor}" \
-H "Authorization: Bearer $CHIO_CONTROL_TOKEN")
echo "$resp" | jq -r '.receipts[].id'
cursor=$(echo "$resp" | jq -r '.nextCursor // empty')
[ -z "$cursor" ] && break
doneAnalytics Endpoint
Aggregate counters over the same corpus are available at /v1/receipts/analytics. It accepts the same corpus filters as the query endpoint, plus two aggregation knobs:
| Parameter | Description |
|---|---|
groupLimit | Max rows per grouped dimension. Default 50, server cap 200. |
timeBucket | Time aggregation width. hour or day. Default day. |
{
"summary": {
"totalReceipts": 12,
"allowCount": 9,
"denyCount": 1,
"cancelledCount": 1,
"incompleteCount": 1,
"totalCostCharged": 750,
"totalAttemptedCost": 500,
"reliabilityScore": 0.8181818182,
"complianceRate": 0.9166666667,
"budgetUtilizationRate": 0.6
},
"byAgent": [ { "subjectKey": "ed25519-...", "metrics": {} } ],
"byTool": [ { "toolServer": "shell", "toolName": "bash", "metrics": {} } ],
"byTime": [ { "bucketStart": 1700000000, "bucketEnd": 1700086400, "metrics": {} } ]
}The analytics API does backend-side aggregation. It complements (but is distinct from) any client-side dashboard summaries you compute on top of /v1/receipts/query. Use analytics when you want a single round-trip summary, use the raw query when you need the receipts themselves.
Operator Report Endpoint
The composed operator report at /v1/reports/operator packages analytics, cost attribution, and budget utilization into a single response. It is the stable operator workflow surface; dashboards should prefer it to re-composing three separate queries on the client.
It accepts the same corpus filters as the analytics endpoint, plus:
| Parameter | Description |
|---|---|
attributionLimit | Max detailed rows in the nested cost-attribution slice. Default 100. |
budgetLimit | Max budget-utilization rows. Default 50, server cap 200. |
{
"generatedAt": 1700000000,
"filters": {
"agentSubject": "ed25519-...",
"toolServer": "shell",
"toolName": "bash"
},
"activity": { "/* same shape as /v1/receipts/analytics */": null },
"costAttribution": { "/* same shape as /v1/reports/cost-attribution */": null },
"budgetUtilization": {
"summary": {
"matchingGrants": 3,
"nearLimitCount": 1,
"exhaustedCount": 0
},
"rows": [
{
"capabilityId": "cap-123",
"grantIndex": 0,
"subjectKey": "ed25519-...",
"toolServer": "shell",
"toolName": "bash",
"invocationCount": 12,
"maxInvocations": 20,
"totalCostCharged": 850,
"maxTotalCostUnits": 1000,
"remainingCostUnits": 150,
"nearLimit": true,
"exhausted": false,
"scopeResolved": true
}
]
},
"compliance": {
"matchingReceipts": 12,
"evidenceReadyReceipts": 11,
"uncheckpointedReceipts": 1,
"checkpointCoverageRate": 0.9166666667,
"lineageCoveredReceipts": 12,
"lineageGapReceipts": 0,
"directEvidenceExportSupported": false,
"childReceiptScope": "omitted_no_join_path",
"proofsComplete": false,
"exportQuery": { "agentSubject": "ed25519-..." },
"exportScopeNote": "tool filters narrow the operator report only; direct evidence export can scope by capability, agent, and time window."
}
}CLI: chio receipt list
The chio receipt list subcommand wraps the HTTP endpoint. It prints each matching receipt as a JSON object on its own line (NDJSON), which composes well with jq and downstream tooling.
chio receipt list [OPTIONS]
Options:
--capability <ID> Filter by capability ID
--tool-server <NAME> Filter by tool server name
--tool-name <NAME> Filter by tool name
--outcome <OUT> allow | deny | cancelled | incomplete
--since <SEC> Minimum timestamp (inclusive)
--until <SEC> Maximum timestamp (inclusive)
--min-cost <UNITS> Minimum cost in minor currency units
--max-cost <UNITS> Maximum cost in minor currency units
--limit <N> Page size (default 50)
--cursor <SEQ> Pagination cursor (seq value)
--control-url <URL> Trust-control server URL
--control-token <TOK> Bearer token
--receipt-db <PATH> Path to local receipt SQLite (local mode)chio receipt list \
--outcome deny \
--since 1700000000 \
--control-url http://localhost:7391 \
--control-token my-tokenTo paginate programmatically from the CLI, capture nextCursor from one invocation and pass it as --cursor on the next. The streaming SDK iterators are easier if you have a choice.
Rate Limits
The trust-control server enforces soft rate limits per bearer token to protect the underlying receipt store from runaway scans. The defaults are operator-configurable, but the shipped shape is:
| Endpoint | Default limit | Burst |
|---|---|---|
/v1/receipts/query | 60 requests per minute per token | 30 buffered |
/v1/receipts/analytics | 12 requests per minute per token | 6 buffered |
/v1/reports/operator | 6 requests per minute per token | 3 buffered |
Over-limit requests return HTTP 429 with a Retry-After header in seconds. The SDK clients honor that header and back off automatically. If you are hitting the limits often, consider moving to /v1/reports/operator for summary dashboards (one request replaces many) or using the streaming iterators to pace your scans.
Error Responses
Errors are JSON objects with a stable shape:
{
"error": {
"code": "invalid_cursor",
"message": "Cursor '147xyz' is not a valid seq value",
"detail": { "cursor": "147xyz" }
}
}| HTTP | Code | Meaning |
|---|---|---|
| 400 | invalid_parameter | A filter value did not parse |
| 400 | invalid_cursor | Cursor is not a valid seq value |
| 401 | unauthorized | Missing or invalid bearer token |
| 403 | forbidden | Token is valid but not authorized for the requested scope |
| 429 | rate_limited | Per-token rate limit exceeded; retry after the header window |
| 500 | internal_error | Server-side failure; safe to retry after a short backoff |
Operational Notes
- Ordering. Receipts are returned by
seqascending within a page. Across pages, new receipts append at higherseqvalues, so a long-running forward pagination will eventually see new data without repeating old data. - Signature verification. Every receipt is signed. Clients that care about audit integrity should verify the signature client-side using the kernel's published key before trusting the contents. The server signs the transport envelope too, but the receipt signature is the one that survives in any downstream copy.
- Backfill. If the kernel has a large historical log, the first full scan can take a while. Start with a tight
sincebound and expand the window as you catch up. The operator report endpoint is a faster way to get a current-state picture. - Availability. Trust-control is designed as a high-read service. It can serve reads while the underlying kernel is busy writing new receipts; there is no global lock between read and write paths.
Prefer the SDK where possible
Related References
- Receipt Format is the schema for individual
ChioReceiptobjects returned by this API. - Receipt Dashboard shows how the hosted UI uses this endpoint and the operator report.
- SDK Overview covers the client libraries that wrap this API.
- CLI Reference documents
chio receipt listand related subcommands.