ReferenceSpec
Receipt Query API
HTTP endpoints for reading signed tool-call receipts with filters, cursor pagination, authentication, schemas, and SDK examples.
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. |
The server cap is authoritative
limit at 200 per response and default 50; values above the cap are silently reduced. The TypeScript and Python ReceiptQueryClient helpers forward your limit unchanged and follow nextCursor — they do not add a higher logical cap or a transparent double-fetch. Plan bandwidth around 200-item pages.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. The constructor takes positional arguments: new ReceiptQueryClient(baseUrl, authToken, fetchImpl?). query(params) fetches a single page; paginate(params) is an async generator that yields one array of receipts per page, following nextCursor for you.
import { ReceiptQueryClient } from "@chio-protocol/sdk";
const client = new ReceiptQueryClient(
"https://trust.example.com",
process.env.CHIO_CONTROL_TOKEN!,
);
// Single page.
const page = await client.query({
outcome: "deny",
since: 1700000000,
limit: 50,
});
console.log(`${page.totalCount} total denies`);
for (const receipt of page.receipts) {
console.log(receipt.id, receipt.decision);
}
// paginate() yields one array of receipts per page.
for await (const receipts of client.paginate({
toolServer: "payment-server",
since: 1700000000,
})) {
for (const receipt of receipts) {
if (receipt.decision?.verdict === "deny") {
console.warn(`denied: ${receipt.decision.reason}`);
}
}
}Python SDK
The chio.ReceiptQueryClient constructor is ReceiptQueryClient(base_url, auth_token, *, client=None) — note the keyword is auth_token, not bearer_token. Its query() and paginate() methods are synchronous (blocking urllib calls); there is no await and no async for. Filters are passed as a dict of the same camelCase keys the HTTP endpoint accepts, and paginate() yields one list of receipts per page.
import os
from chio import ReceiptQueryClient
client = ReceiptQueryClient(
"https://trust.example.com",
auth_token=os.environ["CHIO_CONTROL_TOKEN"],
)
# Single page. query() and paginate() are synchronous.
page = client.query({"outcome": "deny", "since": 1700000000, "limit": 50})
print(f"{page['totalCount']} total denies")
for receipt in page["receipts"]:
print(receipt["id"], receipt.get("decision"))
# paginate() yields one list of receipts per page.
for receipts in client.paginate({"toolServer": "payment-server", "since": 1700000000}):
for receipt in receipts:
decision = receipt.get("decision")
if decision and decision["verdict"] == "deny":
print(f"denied: {decision['reason']}")Both clients thread the cursor for you and stop when the server returns no nextCursor. They do not refresh tokens, retry on transient errors, or verify receipt signatures; do that in your own code if you need it.
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. Dashboards can use it instead of composing three client-side queries.
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)
--tenant <ID> Local read boundary (mutually exclusive with --admin-all)
--admin-all Read across all tenants (local admin operation)
--control-url <URL> Trust-control server URL
--control-token <TOK> Bearer token
--receipt-db <PATH> Path to local receipt SQLite (local mode)Local reads need an explicit read boundary
--receipt-db mode the read path fails closed unless exactly one of --tenant <ID> or --admin-all is supplied; they are mutually exclusive. Both are rejected when combined with --control-url — remote reads derive their scope from the control token instead.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 SDK paginate() helpers reduce this pagination work.
Rate Limits
The receipt query, analytics, and operator-report endpoints are not rate-limited today. The trust-control router applies a request body-size limit and a response-header layer, but no per-token request-rate middleware sits in front of these read paths. Size your own scans politely: prefer /v1/reports/operator for summary dashboards (one request replaces many) and reuse nextCursor rather than re-scanning from the start.
Error Responses
On every error path — auth failure, a store that cannot be opened, a serialization failure — these handlers return a JSON object with a single error string. There is no nested code/detail object and no machine-readable error-code registry on this endpoint today. Read the HTTP status for the class of failure and the string for the detail.
{ "error": "receipt point-load by id requires the admin service token" }| HTTP | Meaning |
|---|---|
| 401 | Missing or invalid bearer token |
| 403 | Token is valid but not authorized for the requested scope |
| 500 | Server-side failure (store open, serialization); the string carries the detail |
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.