A receipt is a signed record of how a protected action ended. It points at the
chain of signed objects behind the action — the request, the approval or rejection, the execution and
its result — and comes with an evidence bundle (a .kbundle file) that anyone can verify offline.
Use receipts when you need to show, after the fact, who authorized a tool call, what exactly was authorized and what the Gateway recorded as the result. This page explains the concepts; to check a receipt step by step, see Verify a receipt.
What a receipt is
Every protected action is a thread of signed K-Block objects. A receipt is one more signed object, of type RECEIPT, that covers a path through that thread, from its first object (the TASK) to the object that ended it (the head). Its payload names:
- the workflow the thread follows, its root and its head;
- the IDs of every object it covers;
- the identity document each signer used (pins);
- the outcome, and a summary of the action.
The receipt is signed by the Gateway's service identity (see Identities and KIDs). Because every object is content-addressed and signed, changing any signed content in the bundle makes verification fail. The dashboard's receipt page also compares the bundle's SHA-256 with the value recorded when the receipt was issued.
When receipts are issued
A receipt is issued when an action reaches a final state that someone signed:
| Final state | Receipt | Head object | Outcome stored | Issued by |
|---|---|---|---|---|
succeeded | Yes | RESULT | succeeded | Gateway |
failed (upstream or Gateway failure) | Yes | RESULT | failed | Gateway |
failed with INTERRUPTED (recovered) | Yes, when the action's evidence still verifies | RESULT | failed | Gateway or Cloud API |
rejected | Yes | REJECTION | final | Cloud API |
cancelled | Yes | CANCELLATION | final | Cloud API |
authorized (decision only) | Yes: an authorization receipt | PROPOSAL, or the person's APPROVAL when one was required | pending or granted | Gateway |
denied | No | — | — | — |
expired (APPROVAL_EXPIRED, TASK_EXPIRED, SUPERSEDED) | No | — | — | — |
executing | Not yet | — | — | — |
Denied and expired actions are recorded in the dashboard and the audit log, but no receipt is issued for them. (An expired action's TASK and PROPOSAL stay stored as the signed record of its decision; nothing is signed for a denial.)
For rejections and cancellations the stored outcome is final, the state of the REJECTION or
CANCELLATION in the workflow. The dashboard shows the action's status instead (for example
"Rejected") in the receipts list, in the receipt page's header and in the action page's receipt card;
the receipt page's Outcome field shows "Final" with the action's status beside it. Filtering the
receipts list in the Cloud API with outcome=rejected finds nothing, because the stored value is
final.
For a decision-only action the receipt is an authorization receipt: it proves that K-Block
authorized the exact call, not that anyone executed it, and it contains no EXECUTION or RESULT. Its
stored outcome is the state of its head, pending for the PROPOSAL or granted for the APPROVAL,
which the dashboard shows as Authorized (decision only). The Cloud API marks every receipt with
its kind: execution, authorization or closure (rejected or cancelled).
Each new receipt sends a receipt.created webhook event, with the same kind (see
Webhook events). The agent gets the receipt's IDs in _meta.kblock
(receipt_id, receipt_object_id) on the answer that reports the outcome; a decision-only caller
gets them in the answer of POST /v1/authorize.
How long K-Block Cloud hosts a receipt
K-Block Cloud hosts a receipt's bundle for the organization's
retention period, counted from the end of its action. Then it
deletes the bundle and the signed objects, and keeps a record of the receipt: its IDs, outcome,
issuer, and the SHA-256 and size of the bundle. Asking for the receipt, its bundle or its decision
check then answers 410 with the code EVIDENCE_EXPIRED: the hosted copy is gone, the receipt is
not invalid.
The bundle itself does not expire. A .kbundle you downloaded carries everything a verifier needs,
so it stays independently verifiable offline for as long as you keep it, and its SHA-256 still
matches the record. Download the receipts you need to keep before the retention period ends.
What the evidence bundle contains
The .kbundle file is a JSON document:
{
"kblock_bundle": "0.1",
"receipt": { /* the signed RECEIPT object */ },
"objects": [ /* the signed objects the receipt covers */ ],
"identities": [ /* identity documents of every signer, and of the issuer */ ]
// "disclosures": optional, revealed payloads of encrypted objects
}The covered objects are the path from the TASK to the head, the delegations the thread acts under (and their revocations, if any), and attestations about objects on the path. For example:
| How the action ended | Path in the bundle |
|---|---|
| Allowed and executed | TASK → PROPOSAL → EXECUTION → RESULT |
| Approved and executed | TASK → PROPOSAL → APPROVAL → EXECUTION → RESULT |
| Rejected | TASK → PROPOSAL → REJECTION |
| Cancelled | TASK → CANCELLATION |
| Authorized for the caller (decision only) | TASK → PROPOSAL, or TASK → PROPOSAL → APPROVAL |
A cancellation receipt covers only TASK → CANCELLATION. A PROPOSAL or APPROVAL that existed before the cancellation is not on that path, so the receipt says nothing about it.
Signatures and identities
Each object in the bundle is signed by the identity that acted:
| Object | Signed by |
|---|---|
| TASK, EXECUTION, RESULT | The Gateway's service identity, on behalf of the organization principal under its delegation |
| PROPOSAL | The Gateway's service identity |
| APPROVAL, REJECTION | The approver's personal identity |
| CANCELLATION | The organization principal |
| Delegation, revocation | The organization principal |
| ATTESTATION | A K-Block-aware upstream server's identity |
| RECEIPT | The Gateway's service identity |
The bundle carries the identity documents of every signer, so a verifier needs nothing else to check the signatures. What a verifier must decide for itself is whether it trusts the issuer: a bundle can be internally consistent and still have been issued by an identity other than K-Block's Gateway. That is why the dashboard compares the issuer with the Gateway KID it trusts (its pinned Gateway KID).
When you verify a bundle yourself, compare its issuer with the Gateway KID. The dashboard shows the Gateway KID right after you create a gateway API key. If it does not show it, contact K-Block support.
Storage and download
When a receipt is issued, K-Block verifies the bundle once more and stores it before it records the receipt. With the receipt, K-Block keeps the bundle's SHA-256 and size.
You can get the bundle in two ways:
- In the dashboard, open the receipt under Receipts and select Download .kbundle.
- Through the Cloud API:
GET /api/v1/orgs/<org-id>/receipts/<receipt-id>/bundle. Members of any role can read receipts; a management API key needs thereceipts:readscope.
The Cloud API serves a bundle only after checking its bytes against the recorded SHA-256. The
response carries the hash in the x-kblock-bundle-sha256 header and is named
receipt-<receipt object id>.kbundle. When K-Block cannot read the stored bundle, or the stored
bundle no longer matches its recorded SHA-256, the Cloud API refuses it with HTTP 500, a problem on
K-Block's side. The dashboard then shows "K-Block could not complete this request because of a
problem on its side." with a request ID. You cannot repair a stored bundle yourself, and nothing in
the dashboard repairs it: try again later, and if it continues, contact K-Block support and give them
the request ID (shown in the dashboard's error, or in the response's x-request-id header).
K-Block never deletes receipts in this version: the plan's retention period applies to operational data only, never to receipts or the signed objects they cover. See Data retention.
Verification
Verifying a bundle checks every signature, every identity chain, the workflow rules (which object may follow which, and who may sign it), and that the receipt covers exactly the objects it names. There are three places to do it:
| Where | What it trusts |
|---|---|
| Verification by the Cloud on the receipt page | The Cloud API re-reads the stored bundle and checks its hash on every request, and verifies it (a report is reused for the same bytes while the trust inputs are unchanged). It trusts as attesters the server KIDs currently registered in the organization, and reports invalid if a signer is marked as equivocated |
| Verify in this browser on the receipt page | The K-Block verifier runs in your browser, independently of the Cloud API's verification, and compares the bundle's SHA-256 with the recorded one. The result is VALID only if the Gateway identity the dashboard trusts (its pinned Gateway KID) issued the receipt and signed its thread, and the bundle is the receipt the page shows |
Offline, with @kblockai/sdk/verify | Your own code: you choose the attesters to trust, and you compare the issuer the report names with the Gateway KID you expect. See Receipt verification SDK |
The Receipts page also has Verify a receipt file: choose any .kbundle file (up to 16 MiB) and
it is checked entirely in your browser; nothing is uploaded.
The in-browser check reports one of these verdicts:
| Verdict | Meaning |
|---|---|
| VALID | Every check passed, and the Gateway identity the dashboard trusts (its pinned Gateway KID) issued the receipt and signed its thread |
| CONSISTENT | Every check passed against the identities inside the bundle, but the dashboard has no trusted Gateway identity to compare the issuer with, so the issuer is not established. Compare the issuer KID with the Gateway KID that K-Block gave you, and contact K-Block support if the dashboard keeps showing this verdict |
| NOT THIS GATEWAY | The bundle is consistent, but another identity issued it or signed its thread |
| MISMATCH | The bundle verifies, but it is not the receipt this page shows |
| INVALID | The receipt did not verify, or its SHA-256 differs from the recorded one |
Attestations
A plain upstream MCP server knows nothing about K-Block. For it, the RESULT is the Gateway's own record of what the upstream returned.
An upstream registered with a server KID is K-Block-aware. The Gateway sends it the evidence in
_meta.kblock, and the upstream must answer with an ATTESTATION signed by its own identity. The
Gateway verifies the attestation before recording the result; a missing or invalid attestation makes
the execution fail (ATTESTATION_MISSING, ATTESTATION_INVALID). The attestation is bound into the
RESULT and included in the bundle.
An attestation counts only for a verifier that chooses to trust its attester. The Cloud trusts the server KIDs registered in your organization; an offline verifier decides for itself.
What a receipt proves, and what it does not
A valid receipt shows who signed what, in which order, and under which delegation; that the approver signed exactly this proposal (arguments, tool, server, action, price, upstream URL); and that the result's terms equal the approved terms.
It does not prove:
- the real-world effect of a call to a plain upstream — the RESULT is the Gateway's claim;
- that the timestamps are true;
- the tool binding (credential version, input schema, amount mapping), which only the Gateway checks, against the tool configuration K-Block has stored; it is not in the signed evidence;
- anything about objects outside the covered path;
- that the Cloud API and the Gateway are separate parties: both hold the Gateway's service identity and both sign receipts.
An INTERRUPTED receipt says failed even though the upstream outcome is unknown. Details:
What a receipt proves.