This page describes that document: what is in it, why it is shaped the way it is, and how you verify one on your own machine.
The question a receipt answers#
A Decision Receipt is designed to let any party later prove one precise thing: at time T, agent A acting for company B presented grant C to relying party D for action E; Delegus evaluated it against this identity state, this revocation state, these rules and this trust configuration, and decided X.
It has to keep proving that after keys rotate, after the company changes its DID document, and after the revocation list has been re-signed a thousand times. So the receipt does not link to any of those things. It pins their hashes, and Delegus Check keeps the bytes.
What is inside#
Here is the shape v0.2 specifies (delegus-), trimmed:
{
"receipt_version": "delegus-receipt-v1",
"receipt_id": "drc_01K4Q7ZP9X2M3N4R5S6T7V8W9Y",
"decision": "ALLOW",
"reason": null,
"evaluated_at": "2026-09-09T14:03:27.412Z",
"relying_party": "did:web:delegus.ai:rp:seller",
"principal": { "id": "did:web:acme.com", "verification": "domain" },
"agent": "did:key:z6Mk...",
"grant_id": "urn:delegus:grant:01J9X4...",
"grant_hash": "sha256:...",
"proof_hash": "sha256:...",
"request_hash": "sha256:...",
"authority": { "action": "commerce:purchase",
"constraints": { "maxAmount": 50000, "currency": "USD" } },
"expires_at": "2026-10-01T00:00:00Z",
"evidence": {
"issuer_resolution": { "did": "did:web:acme.com",
"did_document_hash": "sha256:...",
"resolved_at": "2026-09-09T14:00:11Z" },
"status": { "credential_url": "https://status.delegus.ai/acme.com/1",
"credential_hash": "sha256:...",
"checked_at": "2026-09-09T14:03:27.410Z",
"status_list_index": "4127" },
"trust_config_version": "trust_0042",
"trust_config_hash": "sha256:..."
},
"protocol": { "result": "PASS", "profile": "delegus-base-v1",
"profile_hash": "sha256:...",
"checks": { "P1": true, "P2": true, "...": "...", "P20": true } },
"trust": { "result": "PASS", "policy_version": "delegus-trust-v1",
"checks": { "T1": true, "T2": true, "T3": true, "T5": true } },
"receipt": "<compact JWS>"
}Read it as five layers.
1. The decision. decision is ALLOW or DENY. reason is null on ALLOW; on DENY it is the first check that failed, in the fixed evaluation order — a stable string like AMOUNT_, AUTHORITY_ or PROOF_ that your code can switch on. There is one reason, never a list, because the order is part of the contract.
2. The parties and the authority. Who the relying party was, which company issued the grant and how its authority was established ("domain": it verified control of its own domain; "managed": the operator manages its identity; or "unverified"), the agent's key, the grant id, and the specific capability the action matched, with its constraints. One rule worth knowing: on a DENY where the grant's signature did not check out, these attribution fields are omitted. Delegus never echoes an attribution it could not authenticate. Domain verification proves control of a domain, never who an organisation or a person is.
3. What was presented. grant_hash, proof_hash and request_ are SHA-256 over the exact bytes Delegus saw — the compact JWS strings as received, and the canonicalized action. Delegus does not store your grants, proofs of grant or actions; you keep those. The receipt makes it impossible to substitute different ones later.
4. What the world looked like. This is the evidence block, and it is the part that makes offline verification possible. The company's DID document as fetched, the revocation status-list credential as evaluated, and the trust-configuration descriptor in force — each pinned by hash. Not "the current DID document". The one that was there at evaluated_.
5. What rules ran, and how it went. protocol. lists every check P1 through P20 (P1 through P21 and D1 through D7 on a v0.3 receipt) with true, false or "skipped", and profile_ is the hash of the published profile document that defines them. trust. is the same for T1, T2, T3 and T5, Delegus's own trust checks (is this issuer known, verified, not flagged; has this proof of grant been seen before).
Then receipt: a non-detached compact JWS over the JCS-canonicalized body, alg: EdDSA, typ: delegus-, kid: did:. The JWS string is the canonical receipt. The JSON is a convenience and must equal the payload.
One hash encoding, and why it is boring on purpose#
Every hash in the receipt is sha256: over defined bytes. No multihash, no base64url, no "canonical JSON except when". The evidence store's object keys are the hashes themselves, so GET / returns bytes whose hash is, by construction, the key you asked for. A key can never disagree with its content, so there is nothing to argue about.
Re-verifying a receipt without Delegus#
Given the receipt, Delegus's receipt public key (published in the DID document at delegus.; rotation adds receipt- and never removes receipt-), and the artifacts the receipt names by hash (did_, credential_, profile_, trust_, each fetched from GET /), a third party re-runs the open protocol checks and confirms protocol.. This works for v0.2 receipts (delegus-) and for v0.3 receipts (delegus-, delegus-) alike.
A v0.3 receipt carries two things only its relying party can open: the authority_ (a commitment to the Capability that matched; its opening is returned beside the receipt) and, when a budget was spent, the consumption snapshot and capability_. If an action rested on earlier receipts (relies_on), the dependency checks D1 to D7 need those receipts and their openings too. A re-run without that material still re-runs P1 to P20 and still checks every pinned hash; it reports the rest as not reproduced and says why. It never reports a commitment it could not open as a mismatch. With the /verify response as the relying party stores it, the openings beside the receipt reproduce the commitments as well.
Two details make this work.
The clock is in the receipt. Time-dependent checks — is the grant inside its validity window, is the proof of grant fresh — must be evaluated with now set to the receipt's own signed evaluated_, never the verifier's wall clock. v0.2 makes this normative. Because evaluated_ is inside the signed payload, nobody can move the evaluation time without breaking the signature. So a receipt for a grant that expired in 2026 still reproduces ALLOW in 2031, correctly, because the question is whether it was valid at the time.
The trust block is not reproducible, by design. protocol. are the open, published rules; anyone can re-run them. trust. are Delegus's attestation — that the issuer was registered and domain-verified, that the proof of grant's jti had never been consumed before. You cannot recompute those from public inputs, and the receipt does not pretend you can. What you can verify is that Delegus Check signed that attestation, at that time, over those exact inputs.
That split is the honest line between an open format and a run service: anyone can verify a Delegus artifact; only Delegus can produce a fresh one.
Check a receipt here#
Paste a receipt. The check runs in your browser against the key Delegus publishes; it does not re-run the decision.
Doing it#
No API key is needed. The CLI wraps the same code as the SDK.
npm i -D @delegus/sdk
npx delegus receipt verify --receipt receipt.json # signature under the DID document, the public check, and what it does not confirm
npx delegus receipt verify --receipt receipt.json \
--grant grant.jws --proof proof.jws --action action.json # the re-run: every pinned hash, every open check, now = evaluated_atThe re-run prints hashes (match, mismatch, not supplied, not in receipt), reproduced (protocol, decision, differences), not_ (what and why), and two plain lists: confirms and not_. Exit code 0 when nothing contradicts the receipt, 2 when something does. Pass - (one file per hash) to run with no network at all. In code: await delegus., with r. naming what needs entitled material.
Why DENY receipts matter as much as ALLOW receipts#
It is tempting to think of the receipt as evidence of authority. It is more useful as Delegus Check's signed record of the decision. When a company later says "our agent never had authority to do that", a DENY receipt with AMOUNT_ and the hash of the exact request is the seller's answer. When a seller says "we were never told the grant was revoked", a receipt pinning the status-list credential as it stood — and the spec's rule that a successful /revoke is seen by every later /verify — is the company's answer.
A DENY is billed like an ALLOW for the same reason. Both produce a signed receipt, and the receipt and its evidence are kept for seven years in write-once storage that no one, including Delegus, can change; both are the thing you will want in the room when someone asks who let that happen.
What is retained, and for how long#
The v0.2 specification (§8) sets the obligation: retain, addressable by hash, every DID document snapshot, status-list credential snapshot, profile document and trust-configuration descriptor referenced by any receipt, and the receipt itself and its outcome ledger, for seven years from evaluated_.
What runs in production: every receipt issued from 25 Sep is written to write-once storage (Backblaze B2 Object Lock in compliance mode, seven years) before the answer is returned. Receipts and evidence stored before we moved to this storage on 4 November 2026 were copied to it, and each one was checked by its hash. The evidence a receipt references is locked the same way, and a daily job extends the lock on any evidence a newer receipt relies on, so everything a receipt references stays locked for at least seven years after that receipt. No one, including Delegus, can delete or change them while the lock holds. The production database can be restored to any point in the last seven days; longer backups under a seven-year lock are planned. Outcomes are kept in the service's database.
Outcomes are events on the receipt — fulfilled, disputed, resolved — append-only, never overwritten. Over time that becomes a record of authority, decision, reliance and outcome, each step signed by the party that took it.
Try it against a real one#
The live demo at delegus.ai/demo runs the handshake against the development sandbox and hands you real receipts, ALLOW and DENY, signed by real KMS-held keys. Take one, take the grant and the proof of grant it was issued for, and run the command above. The specification is at delegus.ai/docs/spec; the profile document whose hash appears in every receipt is served byte-for-byte at delegus.ai/profiles/delegus-base-v1.
Receipt v2 in v0.3#
Release candidate. This describes v0.3-rc1, the current release candidate. New and existing relying parties are on it (profile
delegus-). It is not frozen: the commitment construction is awaiting an external cryptography review, and the v0.2 specification stays the normative text until v0.3 is frozen. Details can still change.base- v3
Receipt v2: what changes in v0.3#
v0.3 introduces a second receipt format, delegus-. A relying party receives it once it moves to the v0.3 rules (the profile delegus-). v1 receipts are unchanged and keep verifying under the v0.2 rules.
v2 is designed so that a receipt reveals no more than it needs to in order to be checked. A v1 receipt carries the matched capability in plaintext, so anyone holding it can see what the agent was allowed to do, up to what amount and in what currency. A v2 receipt carries a commitment to it instead. That's a hash over the capability together with a 32-byte random secret (the opening). Someone holding the commitment and the opening can confirm it matches a given capability. Someone holding only the receipt learns that a capability matched, and nothing about what it said.
A plain hash would not be enough. A capability has little variety (an action type, an amount, a currency, a few patterns), so anyone could hash likely guesses and compare. The random opening stops that.
Here is the v2 shape, trimmed. Field names are the ones the engine emits.
{
"receipt_version": "delegus-receipt-v2",
"receipt_id": "drc_01K4Q7ZP9X2M3N4R5S6T7V8W9Y",
"decision": "ALLOW",
"reason": null,
"evaluated_at": "2026-09-09T14:03:27.412Z",
"relying_party": "did:web:delegus.ai:rp:seller",
"principal": { "id": "did:web:acme.example", "verification": "domain" },
"agent": "did:key:z6Mk...",
"grant_id": "urn:delegus:grant:01J9X4...",
"grant_hash": "sha256:...",
"proof_hash": "sha256:...",
"request_hash": "sha256:...",
"authority_commitment": "sha256:...",
"transaction": "<22 characters>",
"requires_hash": "sha256:...",
"relies_on_hash": "sha256:...",
"expires_at": "2026-10-01T00:00:00Z",
"evidence": {
"issuer_resolution": { "did": "did:web:acme.example", "did_document_hash": "sha256:...", "resolved_at": "..." },
"status": { "credential_url": "https://status.delegus.ai/acme.example/1", "credential_hash": "sha256:...",
"checked_at": "...", "status_list_index": "4127" },
"consumption": { "snapshot_hash": "sha256:...", "as_of": "...",
"capability_key_commitment": "sha256:...", "capability_index": 0, "consumed": "consumed" },
"trust_config_version": "trust_0042",
"trust_config_hash": "sha256:..."
},
"protocol": { "result": "PASS", "profile": "delegus-base-v3", "profile_hash": "sha256:...",
"checks": { "P1": true, "...": "...", "P21": true, "D1": true, "...": "...", "D7": true } },
"trust": { "result": "PASS", "policy_version": "delegus-trust-v3",
"checks": { "T1": true, "T2": true, "T3": true, "T5": true, "T7": true } },
"receipt": "<compact JWS>"
}For a relying party on delegus-, trust_ is trust_0003, the descriptor the service loads for that profile.
What is new, field by field:
| Field | In plain words | Present |
|---|---|---|
authority_commitment | The capability that matched, hidden under a commitment. Replaces v1's plaintext authority. | Whenever a capability matched, ALLOW or DENY: on ALLOW, and on a DENY decided after the match (P21, D1 to D7, a replay at T5, T7). Absent when no capability matched (a DENY at P1 to P14, T1 to T3, P15, or at P16 to P20 with no match), and, like the other attribution fields, when the Grant's signature did not verify. |
evidence.consumption.capability_key_commitment | For a capability with a budget: the internal key of that capability's spending record, hidden the same way. Replaces v1's plaintext capability_key. | When the matched capability has a budget |
transaction | A handle that lets this relying party group its own receipts from one deal. 22 characters, random. | ALLOW only |
relies_on_hash | A hash over the earlier decisions this action declared it depends on, with their per-entry results | When dependencies were evaluated (see Relying on earlier decisions) |
requires_hash | A hash over the dependencies the relying party required | Exactly when the relying party sent requires |
protocol.checks.P21, D1–D7; trust.checks.T7 | Budget headroom, the dependency checks, and the budget spend | Always under delegus-base-v3 |
The two commitments use the same framing as dependency fingerprints, each with its own label so a value made for one purpose can never pass as another:
authority_commitment = SHA-256( u8(20) ‖ "delegus:authority:v1" ‖ opening_a ‖ u64be(len(C)) ‖ C )
capability_key_commitment = SHA-256( u8(25) ‖ "delegus:capability-key:v1" ‖ opening_k ‖ u64be(len(K)) ‖ K )
C = UTF-8 of JCS(the matched capability), exactly as it appears in the Grant
K = ASCII of the capability's key: "sha256:" + 64 lowercase hex, which is sha256(JCS(C))
opening_a, opening_k = independent 32-byte random values, never reusedA v2 receipt stays small whatever the Grant contains or however many dependencies it declares: its signed content must fit in 4,096 bytes, and if it would not, Delegus Check refuses with HTTP 422 RECEIPT_, issues no receipt, and neither uses up the Proof nor spends budget.
What comes back beside the receipt#
The /verify response to a v0.3 relying party carries the receipt, plus some members outside the signed receipt. They are for the relying party only, and the receipt pins each of them, so a client can check them against it.
| Member | What it is | Present | How to check it |
|---|---|---|---|
authority | The matched capability, in plaintext | Exactly when the receipt carries authority_commitment | authority_opening opens authority_commitment to it |
authority_opening | The opening of authority_commitment: 43 characters, base64url | Same | as above |
capability_key_opening | The opening of evidence.consumption.capability_key_commitment | Exactly when the receipt carries that commitment | Opens it to sha256(JCS(authority)) |
relies_on | The declared dependencies with their per-entry results: { fingerprint, mode, checks: { D2…D7 }, evidence_commitment? } | Exactly when the receipt carries relies_on_hash | sha256(JCS(relies_on)) equals relies_on_hash |
opening | { fingerprint, nonce }: the fingerprint a later action can declare to rely on this receipt, and its opening | ALLOW only | The nonce opens the fingerprint for this receipt's JWS |
The SDK checks every one of these when you call verify with protocol: "v0., and throws ResponseIntegrityError rather than return anything that does not match. If you call the HTTP API directly, treat a response whose beside-members do not match the receipt as invalid.
On the AuthZEN route (POST /), a relying party on delegus- receives the same members under context., beside context..
Keep the whole response. The receipt is the record, and the members beside it are what let you show later what it meant. opening in particular is given once: pass its fingerprint to whoever will declare a dependency on this decision, and never forward its nonce.
Who gets the openings#
A receipt in v0.3 belongs to two parties: its relying party (the DID in relying_) and its principal (the company in principal., whose agent acted). They are the receipt's entitled parties. Each reaches Delegus through one of:
- a key of that relying party (for the relying party),
- a key of that principal (for the principal),
- a key of an organization that owns that relying party or that principal,
- or Delegus staff.
A DID counts only in its own role: a relying party key never reads receipts as a principal, even at the same DID. Anyone else, asking about a receipt that does not exist, or asking for something the receipt does not have, gets the same HTTP 404, so the answer never confirms that a receipt exists.
| Opening | Opens | Given at /verify to | Can be fetched again by |
|---|---|---|---|
authority_opening | authority_commitment | the relying party | either entitled party: GET /decisions/{receipt_id} and GET /decisions/{receipt_id}/disclosure |
capability_key_opening | evidence.consumption.capability_key_commitment | the relying party | same |
opening.nonce | this receipt's dependency fingerprint | the relying party, ALLOW only | only inside a later receipt's disclosure package, and only to a caller entitled to this receipt. No route returns a receipt's own fingerprint and nonce on their own; GET /decisions/{receipt_id} does not include them. |
evidence_nonce | a later receipt's relies_on[].evidence_commitment (CURRENT_AUTHORITY entries) | nobody (not in the /verify response) | the later receipt's entitled parties, in its disclosure package |
The principal receives nothing automatically; it fetches what it is entitled to. Delegus does not control what an entitled party does with what it holds: a relying party or principal can share a receipt, an opening or a package (threat model, NP-12).
Public verification and entitled reproduction#
v0.3 splits checking a receipt into two levels. The principle, from the v0.3 privacy principles (§0.1):
Receipt authenticity is public. Receipt reasoning is entitled.
With only the receipt, anyone can check:
- the signature, against Delegus's published receipt keys;
- that the JSON body equals the signed payload;
- that every v2 field is well formed (commitments and hashes in the
sha256:form, a 22-character handle on an ALLOW, no plaintext capability or capability key); - that the decision, the reason and the check results agree with each other in the v0.3 order.
That shows the receipt is authentic and unmodified. It also shows the parties, the time, the decision and reason, and the per-check results, and that a capability matched and dependencies were evaluated. It does not show what the capability allowed, which earlier decisions were relied on, or why each dependency check came out as it did.
With the Grant, Proof and Action as well, and the public evidence the receipt names by hash, anyone can re-run the open protocol checks P1 to P20 with the receipt's own clock and check every pinned hash: npx delegus receipt verify - (SDK 0.2.1 or later). What needs entitled material (the matched capability, a budget's consumption snapshot, earlier receipts it relied on) is reported as not reproduced, with the reason, never as a mismatch. See Re-verifying a receipt without Delegus.
With the entitled material, an entitled party can also reproduce what the receipt pins:
| To reproduce | You need | From |
|---|---|---|
authority_commitment | authority and authority_opening | the /verify response, or GET /decisions/{receipt_id} |
capability_key_commitment | authority and capability_key_opening | same |
relies_on_hash | the relies_on entries | the /verify response |
requires_hash | the requires you sent | your own records |
| each dependency | the disclosure package, and, if you hold it, the earlier receipt with its opening | GET /decisions/{receipt_id}/disclosure |
| P21 (budget headroom) | the consumption snapshot | same route, as consumption_snapshot |
How much of each dependency an entitled party can check depends on whether it is also entitled to the earlier receipt:
| Part | Party entitled to the later receipt only | Party also entitled to the earlier receipt |
|---|---|---|
| D2 (fingerprint opens to that receipt), D3 (its signature), D4 (it was ALLOW), D6 (not expired) | Delegus's per-entry statement (checks) | reproduced from the earlier receipt and its opening |
| D5 (same deal) | Delegus's statement (transaction_equal), by design: the internal transaction is never disclosed | same |
D7 (the earlier Principal's standing, its signing key's state, and the revocation bit) and evidence_commitment, CURRENT_AUTHORITY only | reproduced from the revocation list and index as read, and evidence_nonce | same |
Time-dependent checks use the receipt's own signed evaluated_ as "now", exactly as in v0.2.
Entitled reproduction, in the SDK and the CLI, checks the commitments, the hashes and the per-dependency results. It does not re-run the checks P1 to P21 from the Grant, Proof and Action, and the CLI refuses --grant, --proof and --action for a v2 receipt. Re-evaluating a v2 receipt offline is not available yet.
Doing it. No API key is needed for either level; the CLI fetches the published receipt keys, or takes them from -.
# Public: anyone holding the receipt
delegus receipt verify --receipt receipt.json
# Entitled: the stored /verify response carries authority, its openings and relies_on
delegus receipt verify --receipt response.json --entitled \
--requires requires.json \
--disclosure disclosure.jsonBoth exit 0 only when every check passes, and 2 otherwise. In entitled mode, a dependency that was not supplied, or could be reproduced only in part, counts as not passing. In code:
const pub = await delegus.receipts.verifyV2Public(receiptJws);
const full = await delegus.receipts.verifyV2Entitled(receiptJws, { authority, authorityOpening, reliesOn, requires, disclosure });
// full.ok, full.authority.result, full.dependencies[i].checks.D7.statusThe full flag and function reference is on SDK and CLI.
The disclosure route#
GET /decisions/{receipt_id}/disclosureCallable by the receipt's entitled parties only (a relying party key, a principal key or an organization key, as above) or Delegus staff. Everyone else, an unknown receipt, and a receipt with nothing to disclose get the same HTTP 404. The response is marked cache-. It is never published by hash, and holding the receipt does not by itself entitle anyone to it.
The response, as the engine builds it:
| Member | Present | Contents |
|---|---|---|
receipt_id | always | the receipt asked about |
authority, authority_opening, capability_key_opening | when the receipt pins the matching commitment | as returned beside the receipt |
consumption_snapshot | when the receipt pins evidence.consumption.snapshot_hash | the counts P21 was evaluated against; sha256(JCS(consumption_snapshot)) equals snapshot_hash |
relies_on[] | ALLOW receipts that declared dependencies (no package is kept for a DENY) | per entry, in declaration order, below |
Each relies_on[] entry:
| Member | Who gets it | What it is |
|---|---|---|
fingerprint, mode, checks | every entitled caller | the entry as the receipt pins it, with D2–D7 as evaluated |
transaction_equal | every entitled caller | D5 as Delegus's statement; null when the fingerprint resolved to nothing |
receipt_hash | every entitled caller | sha256 of the earlier receipt's JWS: names it without revealing it |
status_list_index, status_credential, evidence_nonce | every entitled caller, CURRENT_AUTHORITY only | the revocation list as read, the Grant's position in it, and the opening of evidence_commitment |
receipt, opening | only a caller also entitled to the earlier receipt | the earlier receipt's JWS and its fingerprint's opening |
Being entitled to the later receipt, or having declared a fingerprint, never entitles anyone to the earlier receipt.
Use delegus. (SDK 0.2.0) to fetch it, and pass the JSON to - or verifyV2Entitled(.
What is retained under v0.3#
Under v0.3, receipts are private by default. The artifacts that are public by nature (profile documents, trust-configuration descriptors, DID document snapshots and revocation-list snapshots) stay retrievable by hash by anyone at GET /. A receipt itself, the consumption snapshot it pins and its disclosure package are retrieved only by the receipt's entitled parties, through GET / and GET /. GET / does not serve consumption snapshots. Nothing is listed or searchable by fingerprint or transaction handle. Receipts and the evidence they reference are kept for seven years in write-once storage that no one, including Delegus, can change; evidence is locked from when it was first stored.