Chio/Docs

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?

The dashboard consumes this same API. See Receipt Dashboard for how the hosted UI wires up to the trust-control server.

Endpoint Summary

MethodPathPurpose
GET/v1/receipts/queryPrimary receipt query with filters and cursor pagination
GET/v1/agents/{subject_key}/receiptsAgent-scoped convenience endpoint; accepts limit and cursor only
GET/v1/receipts/analyticsAggregate counters and groupings over the same corpus
GET/v1/reports/operatorComposed 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:

bash
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

Trust-control tokens grant read access to the full audit trail. Store them in a secrets manager, rotate on personnel change, and never commit them to source control. The receipt query API is a privileged surface.

Filter Parameters

All parameters are query-string parameters. Names are camelCase on the wire, matching the server's serde(rename_all = "camelCase") struct.

ParameterTypeDescription
capabilityIdstringExact match on capability ID
toolServerstringExact match on tool server name (server_id)
toolNamestringExact match on tool name
outcomestringOne of allow, deny, cancelled, incomplete
sinceu64Only receipts with timestamp >= since (unix seconds, inclusive)
untilu64Only receipts with timestamp <= until (unix seconds, inclusive)
minCostu64Minimum cost_charged in minor units. Receipts without financial metadata are excluded when set.
maxCostu64Maximum cost_charged in minor units. Receipts without financial metadata are excluded when set.
agentSubjectstringHex-encoded Ed25519 agent subject key. Resolved from attribution metadata or capability lineage.
cursoru64Pagination cursor. Returns only receipts with seq > cursor (exclusive).
limitusizeMax results per page. Default 50. Server cap is 200; values above the cap are silently reduced.

Some SDKs expose a higher logical cap

Some client SDKs present a logical cap of 500 items per page and paginate under the hood when the server cap is lower. The wire protocol's server-side maximum is 200 per response; the SDK is doing two round-trips if you ask for 500. Plan bandwidth around 200-item pages.

Response Shape

response.json
{
  "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.

bash
# 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

bash
GET /v1/receipts/query?outcome=deny&since=1700000000&limit=2
Authorization: Bearer my-service-token
example-response.json
{
  "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:

bash
GET /v1/agents/{subject_key}/receipts?limit=50&cursor=0
Authorization: Bearer my-service-token

This 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.

audit.ts
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

audit.py
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:

bash
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
done

Analytics 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:

ParameterDescription
groupLimitMax rows per grouped dimension. Default 50, server cap 200.
timeBucketTime aggregation width. hour or day. Default day.
analytics-response.json
{
  "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:

ParameterDescription
attributionLimitMax detailed rows in the nested cost-attribution slice. Default 100.
budgetLimitMax budget-utilization rows. Default 50, server cap 200.
operator-report.json
{
  "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.

bash
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)
bash
chio receipt list \
    --outcome deny \
    --since 1700000000 \
    --control-url http://localhost:7391 \
    --control-token my-token

To 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:

EndpointDefault limitBurst
/v1/receipts/query60 requests per minute per token30 buffered
/v1/receipts/analytics12 requests per minute per token6 buffered
/v1/reports/operator6 requests per minute per token3 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.json
{
  "error": {
    "code":    "invalid_cursor",
    "message": "Cursor '147xyz' is not a valid seq value",
    "detail":  { "cursor": "147xyz" }
  }
}
HTTPCodeMeaning
400invalid_parameterA filter value did not parse
400invalid_cursorCursor is not a valid seq value
401unauthorizedMissing or invalid bearer token
403forbiddenToken is valid but not authorized for the requested scope
429rate_limitedPer-token rate limit exceeded; retry after the header window
500internal_errorServer-side failure; safe to retry after a short backoff

Operational Notes

  • Ordering. Receipts are returned by seq ascending within a page. Across pages, new receipts append at higher seq values, 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 since bound 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

The TypeScript and Python SDK clients handle pagination, retries, signature verification, and error mapping for you. Roll your own only if you are in a language without an SDK or you have a specific reason to bypass the client.

  • Receipt Format is the schema for individual ChioReceipt objects 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 list and related subcommands.