Skip to Content
API referenceSigned governance receipts

Signed governance receipts

When the broker decides a call, it writes the audit row and a signed receipt in the same database write. The receipt is a compact JWS (ES256, typ: agentvalet-receipt+jwt) whose jti is the audit row’s id. Anyone holding the receipt can verify what AgentValet decided, when, for which agent, under which policy version, using only the public key set below. No database access, no AgentValet account.

Receipts are issued for every terminating outcome on POST /v1/actions: allowed, held for approval, denied by grant, policy or guardrail, blocked by response screening, refused for secrets or size, errored on redaction. They are also issued for every approval replay (executed, denied at replay, failed). Denials get receipts too: proof that a call was refused is usually the more valuable evidence.

Finding the audit id

The call itself tells you:

  • On a 2xx, _meta.audit_id in the response envelope.
  • On a refusal (400, 403, 409, 412, 502) or a hold (202), audit_id in the body next to correlation_id.
  • In an MCP host, the receipt card’s audit_id field.

It is a uuid, never the receipt itself, so the model’s context is not spent on 800 bytes of base64 per call.

Fetching the receipt

GET /v1/audit/ff505bfd-6a3f-4b1d-85a0-35b89d431bff/receipt Authorization: Bearer <agent JWT or owner session>
{ "audit_id": "ff505bfd-6a3f-4b1d-85a0-35b89d431bff", "kid": "av-receipt-2026-09b", "receipt": "eyJhbGciOiJFUzI1NiIsInR5cCI6ImFnZW50dmFsZXQtcmVjZWlwdCtqd3QiLCJraWQiOiJhdi1yZWNlaXB0LTIwMjYtMDliIn0...", "jwks_url": "https://api.agentvalet.ai/.well-known/agentvalet-receipt-keys.json", "did": "did:web:api.agentvalet.ai", "verify_hint": "Compact JWS. Verify with alg ES256, typ agentvalet-receipt+jwt, iss https://api.agentvalet.ai, and a key from jwks_url ..." }

An agent bearer may fetch receipts for its own rows only; an owner session may fetch any row in its organisation. Accept: application/jose returns the bare JWS. 404 for a row that has no receipt: rows written before 16 September 2026, broker-ingested rows, Observe Mode relays, connect events and circuit-breaker transitions. Receipts also appear as the last column of the audit export (Team and above).

What the receipt says

{ "iss": "https://api.agentvalet.ai", "sub": "agt_7o00saqajyahinuuc6g83", "jti": "ff505bfd-6a3f-4b1d-85a0-35b89d431bff", "iat": 1789553665, "av": { "v": 1, "trace_id": "287bf627-cbd1-44d2-97fa-80402981b137", "org_id": "360c08d0-…", "owner_id": "…", "agent_id": "agt_7o00saqajyahinuuc6g83", "parent_agent_id": null, "session_id": "…", "session_id_verified": false, "platform": "github", "connection_id": null, "credential_source": null, "caller_hash": null, "caller_verified": null, "scope": "github:repo.read", "method": "GET", "endpoint": "/user", "result": "denied", "decision": { "reason": "endpoint_scope_mismatch", "policy_id": "5604561a-…", "policy_version": 6184, "guardrail_id": null, "matched": [{ "kind": "policy_allow_matched", "policyId": "5604561a-…", "rule": { "targetType": "platform", "targetValue": "github" } }], "scope_unmapped": false, "screen": null, "containment": null, "constraint": null, "reach": null }, "approval": null, "agent_reason_present": null, "agent_reason_sha256": null, "pii": { "detected": false, "redacted": false, "categories": [] }, "upstream_status": null, "row_hash": "sha256:119da7d8…" } }
FieldMeaning
trace_idThe correlation_id the agent was handed, on every plan
parent_agent_idSet for a child agent
session_id_verified: falseThe session id is asserted by the caller and never verified; it is attribution, not identity
endpointPath only, never the query string
decision.matchedThe policy or guardrail rule events that decided the call, from the kernel’s trace
approvalOn a replay: approval id, who approved, when, via which channel, and the pending_receipt_jti of the receipt issued when the call was first held, so the chain is explicit
agent_reason_sha256A hash of the agent’s free-text justification, so an auditor can match it to the row without the text travelling
row_hashSHA-256 over the stored row’s identity fields and iat; an auditor with database access can additionally check the row was not edited after signing

Never in a receipt: request or response bodies, IP address, user agent, geolocation, the free-text agent reason, connection labels (often an email), recipient addresses, the full policy trace, or any credential. A receipt is designed to leave the building.

Verifying offline

Keys are published in two forms: an RFC 7517 JWK Set at https://api.agentvalet.ai/.well-known/agentvalet-receipt-keys.json (active and retired keys, each with a kid), and as did:web:api.agentvalet.ai#receipt-<kid> verification methods in the root DID document at /.well-known/did.json. The set is deliberately separate from the OAuth bearer key at /v1/oauth/jwks.json.

TypeScript (jose):

import { createRemoteJWKSet, jwtVerify } from "jose"; const JWKS = createRemoteJWKSet(new URL("https://api.agentvalet.ai/.well-known/agentvalet-receipt-keys.json")); export async function verifyReceipt(jws: string) { const { payload, protectedHeader } = await jwtVerify(jws, JWKS, { issuer: "https://api.agentvalet.ai", typ: "agentvalet-receipt+jwt", algorithms: ["ES256"], }); return { audit_id: payload.jti, agent: payload.sub, kid: protectedHeader.kid, ...(payload.av as object) }; }

Python (PyJWT):

import jwt from jwt import PyJWKClient JWKS = PyJWKClient("https://api.agentvalet.ai/.well-known/agentvalet-receipt-keys.json") def verify_receipt(jws: str) -> dict: if jwt.get_unverified_header(jws).get("typ") != "agentvalet-receipt+jwt": raise ValueError("not an AgentValet receipt") key = JWKS.get_signing_key_from_jwt(jws).key claims = jwt.decode(jws, key, algorithms=["ES256"], issuer="https://api.agentvalet.ai", options={"require": ["iss", "sub", "jti", "iat"]}) return {"audit_id": claims["jti"], "agent": claims["sub"], **claims["av"]}

Check alg, typ, iss and that kid is in the key set; then trust the payload.

Honest limits

  • A signature proves a row existed and what it said at decision time. It does not prove a row was never deleted; a signed daily checkpoint that would make deletion detectable is planned, not built. Do not read “receipt” as “tamper-evident chain”.
  • Receipts cover decisions made by the AgentValet proxy on /v1/actions and approval replay. Events decided elsewhere (the open-core broker, Observe Mode, dashboard mutations) carry none.
  • Receipts are retained and purged with their audit row, per your plan’s retention window. Export them if you need them to outlive it.
  • Key rotation keeps retired public keys published, so an old receipt keeps verifying; a key that is ever removed would orphan the receipts it signed, which is why removal is gated on a count of rows still referencing it.
Last updated on