Chio/Docs

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?

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 available receipt 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 API.

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.

The server cap is authoritative

The server caps 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

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

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

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

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. Dashboards can use it instead of composing three client-side queries.

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)
  --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

In local --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.
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 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.json
{ "error": "receipt point-load by id requires the admin service token" }
HTTPMeaning
401Missing or invalid bearer token
403Token is valid but not authorized for the requested scope
500Server-side failure (store open, serialization); the string carries the detail

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 cursor threading and map non-2xx responses to typed query errors 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. Signature verification and retries are your responsibility either way.

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