DelegusDocsv0.3 rc1

Docs/Receipts

Guide

The receipt is the product: how a Delegus decision stays checkable for seven years

Most authorization systems give you a decision and a log line. Delegus gives you a decision and a signed document: anyone holding it can confirm it's genuine for years, and its parties can check it against the exact state of the world it was made in.

Try it: change one character. The signature check runs a SHA-256 fingerprint and an Ed25519 verify in your browser; change one character of the receipt and it fails.

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-receipt-v1), trimmed:

json
{
  "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_EXCEEDS_AUTHORITY, AUTHORITY_REVOKED or PROOF_REPLAYED 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_hash 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_at.

5. What rules ran, and how it went. protocol.checks 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_hash is the hash of the published profile document that defines them. trust.checks 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-receipt+jwt, kid: did:web:delegus.ai#receipt-<n>. 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:<64 lowercase hex> over defined bytes. No multihash, no base64url, no "canonical JSON except when". The evidence store's object keys are the hashes themselves, so GET /evidence/sha256:<hex> 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.ai/.well-known/did.json; rotation adds receipt-<n+1> and never removes receipt-<n>), and the artifacts the receipt names by hash (did_document_hash, credential_hash, profile_hash, trust_config_hash, each fetched from GET /evidence/sha256:<hex>), a third party re-runs the open protocol checks and confirms protocol.result. This works for v0.2 receipts (delegus-base-v1) and for v0.3 receipts (delegus-base-v3, delegus-receipt-v2) alike.

A v0.3 receipt carries two things only its relying party can open: the authority_commitment (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_key_commitment. 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_at, never the verifier's wall clock. v0.2 makes this normative. Because evaluated_at 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.checks are the open, published rules; anyone can re-run them. trust.checks 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.

text
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_at

The re-run prints hashes (match, mismatch, not supplied, not in receipt), reproduced (protocol, decision, differences), not_reproduced (what and why), and two plain lists: confirms and not_confirmed. Exit code 0 when nothing contradicts the receipt, 2 when something does. Pass --did-document did.json --evidence ./artifacts/ (one file per hash) to run with no network at all. In code: await delegus.receipts.reverify({ receipt, grant, proof, action }), with r.notReproduced 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_EXCEEDS_AUTHORITY 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_at.

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-base-v3). 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.

Receipt v2: what changes in v0.3#

v0.3 introduces a second receipt format, delegus-receipt-v2. A relying party receives it once it moves to the v0.3 rules (the profile delegus-base-v3). 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.

json
{
  "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-base-v3, trust_config_version is trust_0003, the descriptor the service loads for that profile.

What is new, field by field:

FieldIn plain wordsPresent
authority_commitmentThe 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_commitmentFor 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
transactionA handle that lets this relying party group its own receipts from one deal. 22 characters, random.ALLOW only
relies_on_hashA hash over the earlier decisions this action declared it depends on, with their per-entry resultsWhen dependencies were evaluated (see Relying on earlier decisions)
requires_hashA hash over the dependencies the relying party requiredExactly when the relying party sent requires
protocol.checks.P21, D1–D7; trust.checks.T7Budget headroom, the dependency checks, and the budget spendAlways 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:

text
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 reused

A 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_TOO_LARGE, 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.

MemberWhat it isPresentHow to check it
authorityThe matched capability, in plaintextExactly when the receipt carries authority_commitmentauthority_opening opens authority_commitment to it
authority_openingThe opening of authority_commitment: 43 characters, base64urlSameas above
capability_key_openingThe opening of evidence.consumption.capability_key_commitmentExactly when the receipt carries that commitmentOpens it to sha256(JCS(authority))
relies_onThe declared dependencies with their per-entry results: { fingerprint, mode, checks: { D2…D7 }, evidence_commitment? }Exactly when the receipt carries relies_on_hashsha256(JCS(relies_on)) equals relies_on_hash
opening{ fingerprint, nonce }: the fingerprint a later action can declare to rely on this receipt, and its openingALLOW onlyThe nonce opens the fingerprint for this receipt's JWS

The SDK checks every one of these when you call verify with protocol: "v0.3", 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 /access/v1/evaluation), a relying party on delegus-base-v3 receives the same members under context.delegus, beside context.receipt.

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_party) and its principal (the company in principal.id, whose agent acted). They are the receipt's entitled parties. Each reaches Delegus through one of:

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.

OpeningOpensGiven at /verify toCan be fetched again by
authority_openingauthority_commitmentthe relying partyeither entitled party: GET /decisions/{receipt_id} and GET /decisions/{receipt_id}/disclosure
capability_key_openingevidence.consumption.capability_key_commitmentthe relying partysame
opening.noncethis receipt's dependency fingerprintthe relying party, ALLOW onlyonly 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_noncea 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:

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 --receipt receipt.json --grant grant.jws --proof proof.jws --action action.json (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 reproduceYou needFrom
authority_commitmentauthority and authority_openingthe /verify response, or GET /decisions/{receipt_id}
capability_key_commitmentauthority and capability_key_openingsame
relies_on_hashthe relies_on entriesthe /verify response
requires_hashthe requires you sentyour own records
each dependencythe disclosure package, and, if you hold it, the earlier receipt with its openingGET /decisions/{receipt_id}/disclosure
P21 (budget headroom)the consumption snapshotsame 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:

PartParty entitled to the later receipt onlyParty 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 disclosedsame
D7 (the earlier Principal's standing, its signing key's state, and the revocation bit) and evidence_commitment, CURRENT_AUTHORITY onlyreproduced from the revocation list and index as read, and evidence_noncesame

Time-dependent checks use the receipt's own signed evaluated_at 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 --did-document.

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

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

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

The full flag and function reference is on SDK and CLI.

The disclosure route#

text
GET /decisions/{receipt_id}/disclosure

Callable 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-control: no-store. 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:

MemberPresentContents
receipt_idalwaysthe receipt asked about
authority, authority_opening, capability_key_openingwhen the receipt pins the matching commitmentas returned beside the receipt
consumption_snapshotwhen the receipt pins evidence.consumption.snapshot_hashthe 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:

MemberWho gets itWhat it is
fingerprint, mode, checksevery entitled callerthe entry as the receipt pins it, with D2–D7 as evaluated
transaction_equalevery entitled callerD5 as Delegus's statement; null when the fingerprint resolved to nothing
receipt_hashevery entitled callersha256 of the earlier receipt's JWS: names it without revealing it
status_list_index, status_credential, evidence_nonceevery entitled caller, CURRENT_AUTHORITY onlythe revocation list as read, the Grant's position in it, and the opening of evidence_commitment
receipt, openingonly a caller also entitled to the earlier receiptthe 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.disclosure(receiptId) (SDK 0.2.0) to fetch it, and pass the JSON to --disclosure or verifyV2Entitled({ disclosure }).

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 /evidence/{sha256}. A receipt itself, the consumption snapshot it pins and its disclosure package are retrieved only by the receipt's entitled parties, through GET /decisions/{receipt_id} and GET /decisions/{receipt_id}/disclosure. GET /evidence 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.