DENY if any check is false, and reason is the first false check in this order (spec §5.4). Protocol checks (P) are reproducible by anyone from the receipt's artifacts; trust checks (T) are Delegus's own attestation.Checks in evaluation order#
| # | Check | Passes when | DENY reason | Needs |
|---|---|---|---|---|
| 1 | P1 Protocol | Grant parses and conforms to §3 (incl. parentGrant absent) | GRANT_MALFORMED / UNSUPPORTED_FEATURE | — |
| 2 | T1 Trust | issuer is a registered Principal | ISSUER_UNKNOWN | — |
| 3 | T2 Trust | Principal is domain-verified | ISSUER_UNVERIFIED | T1 |
| 4 | T3 Trust | kid not marked compromised | ISSUER_KEY_COMPROMISED | T1 |
| 5 | P2 Protocol | kid names a verification method present in the DID document snapshot | ISSUER_KEY_NOT_FOUND | T1 |
| 6 | P3 Protocol | Grant signature verifies | GRANT_SIGNATURE_INVALID | P2 |
| 7 | P4 Protocol | now ≥ validFrom | GRANT_NOT_YET_VALID | P3 |
| 8 | P5 Protocol | now < validUntil | GRANT_EXPIRED | P3 |
| 9 | P6 Protocol | Status credential signed by Delegus; bit at statusListIndex is 0 | AUTHORITY_REVOKED | P3 |
| 10 | P7 Protocol | audience, if present, contains RP | AUDIENCE_MISMATCH | P3 |
| 11 | P8 Protocol | Proof parses and conforms to §4 (incl. jti format) | PROOF_MALFORMED | P3 |
| 12 | P9 Protocol | Proof signature verifies against Agent key | PROOF_SIGNATURE_INVALID | P8 |
| 13 | P10 Protocol | Proof iss == Grant sub | PROOF_AGENT_MISMATCH | P8 |
| 14 | P11 Protocol | Proof grant == Grant id | PROOF_GRANT_MISMATCH | P8 |
| 15 | P12 Protocol | Proof aud == RP DID | PROOF_AUDIENCE_MISMATCH | P8 |
| 16 | P13 Protocol | iat within window | PROOF_EXPIRED | P8 |
| 17 | P14 Protocol | action_hash == hash of submitted Action | PROOF_ACTION_MISMATCH | P8 |
| 18 | T5 Trust | Replay: SET replay:{rp_did}:{jti} 1 NX PX {2 × window_ms} returns OK. Executed only after P1–P14 pass, as a separate internal operation from the revocation-version read that precedes P6 (§7). Store unavailable → SERVICE_UNAVAILABLE. | PROOF_REPLAYED / SERVICE_UNAVAILABLE | P1–P14 |
| 19 | P15 Protocol | Action valid for its type | ACTION_MALFORMED | P14 |
| 20 | P16 Protocol | Some Capability matches action.type | ACTION_NOT_AUTHORIZED | P15 |
| 21 | P17 Protocol | All constraint keys known | UNKNOWN_CONSTRAINT | P16 |
| 22 | P18 Protocol | Currency authorized | CURRENCY_NOT_AUTHORIZED | P17 |
| 23 | P19 Protocol | amount ≤ maxAmount | AMOUNT_EXCEEDS_AUTHORITY | P18 |
| 24 | P20 Protocol | resource matches a pattern | RESOURCE_NOT_AUTHORIZED | P19 |
A check whose prerequisite (the Needs column) did not pass is reported as "skipped", so a receipt shows which checks passed before the first failure and which never ran. Between P5 and P6 the service reads the current revocation state, and between P14 and T5 it performs the atomic replay write; those are internal operations, not checks (§5.4). T4 and T6 do not exist; identifiers are never reused (§5.3).
Codes outside the check tables#
| Code | When |
|---|---|
UNSUPPORTED_FEATURE | Returned by P1 for a Grant that uses a reserved field, such as parentGrant, which must be absent in v0 (§3.2). |
ISSUER_NOT_TRUSTED | Reserved. Defined in §5.3, not produced by any v0.2 check. |
SERVICE_UNAVAILABLE | Delegus failed closed because it could not establish a fact it depends on: at T1 (registry unreachable, or a registered Principal whose DID document has not been resolved yet, so there is no identity snapshot to evaluate against), at P6 (current revocation state could not be established, §7) or at T5 (replay store). The affected check is reported false with this reason, and the result is a signed DENY receipt delivered with HTTP 200, not an HTTP error. On /verify, HTTP 503 is used only for failures before evaluation starts (the API key lookup) or after it ends (receipt persistence). /revoke also answers 503 until the revocation is durably recorded and visible to every verify node (§7); a caller retries rather than assuming success. |
What the broad codes cover#
Three codes cover more than their one-line check suggests. These are the engine's readings of the specification, recorded with the engine's source.
GRANT_(P1): besides syntax, an unknown header parameter, claim or member; an unknown Capability action; a lifetime over 90 days;MALFORMED nbf/expnot equal tovalidFrom/validUntil; a Grant over 8 KB.UNSUPPORTED_is onlyFEATURE parentGrantbeing present.PROOF_(P8): besides syntax, anMALFORMED htuthat is not already in §4.3 canonical form (a query or fragment, an uppercase scheme or host, a default port, lowercase percent-escapes, or percent-encoded unreserved characters); ajtioutside 22–64 base64url characters; an unknown claim or header parameter; akidthat is notiss#<multibase>.UNKNOWN_(P17): evaluated over the Capabilities whose action equalsCONSTRAINT action.type. It includesmerchantCategory, which v0 cannot evaluate because the action has no wire field for it.- For
api:call, a method mismatch has no code of its own: methods are applied together with resources at P20, so it isRESOURCE_. P18 and P19 are vacuously true forNOT_ AUTHORIZED api:call.
Refusals without a receipt#
Some requests are refused before any check runs, or because a receipt can’t be issued. These come back as an HTTP error with a JSON body { "error", "message" }, not as a DENY: there is no receipt and no reason. Where the text says so, the Proof is not spent and the agent can retry with it. From the API reference for POST /:
| HTTP | When |
|---|---|
| 400 | Malformed envelope (strings inside it are the engine's business and yield a DENY receipt) |
| 401 | Unknown or non-RP API key |
| 402 | Production checks need a plan for this account (error PLAN_REQUIRED). An access refusal before evaluation, like 401 or 429: no receipt and no DENY reason, and the proof is not spent. Sandbox checks are never refused this way. |
| 413 | Body over 64 KB |
| 422 | The receipt for this decision would exceed the signer's 4,096-byte message limit (error RECEIPT_TOO_LARGE): the Grant's matching capability is too large (for example a very long resources list). Refused before the replay check: no receipt, the Proof is not spent. Narrow the capability. |
| 503 | Backing service unavailable before or after evaluation |
New in v0.3#
These are the codes the v0.3 release candidate adds to the DENY reason codes page. They apply only to a relying party registered on the profile delegus-, unless a row says otherwise. The v0.2 codes on the existing page keep their meaning.
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
A code reaches you in one of two ways:
- In a signed receipt. The check ran and was
false./verifyanswers HTTP 200 with"decision": "DENY", the code inreason, and a signed receipt like any other. - As an HTTP refusal, with no receipt. The request was refused before any check ran, or could not be signed. The body is
{ "error": "<CODE>", "message": ".. Nothing is recorded as a decision, the agent's Proof is not used up, and no budget is spent.. . " }
Codes in a signed receipt#
Evaluation order under delegus-: P1 → T1 → T2 → T3 → P2 … P14 → T5 → P15 … P20 → P21 → D1 → D2 … D7 → T7. reason is the first false check in that order.
| Code | Check | Plain meaning |
|---|---|---|
ISSUER_NOT_TRUSTED | T1 | The company that signed the Grant is registered with Delegus but not in good standing: Delegus staff have suspended it or marked it disputed. |
AUTHORITY_EXHAUSTED | P21 | The Grant has a budget, and this action does not fit in what is left (total, this period's amount, or number of uses). Nothing was spent. See Budgets. |
AUTHORITY_EXHAUSTED | T7 | The action fitted when checked, but another request spent the last of the budget first. Nothing was spent for this request. |
SERVICE_UNAVAILABLE | P21, T7 (new places) | Delegus failed closed: it could not read the budget counts for a budgeted Grant (P21), or could not reach the store to spend (T7). |
DEPENDENCY_NOT_DECLARED | D1 | The relying party required a prior decision (in requires) that the agent's action does not declare, or declares with a different mode. |
DEPENDENCY_COMMITMENT_MISMATCH | D2 | A prior decision the action names does not match any receipt Delegus Check issued. |
DEPENDENCY_RECEIPT_INVALID | D3 | The prior receipt's signature does not verify against a Delegus Check receipt key, or its body does not match what was signed. |
DEPENDENCY_NOT_ALLOW | D4 | The prior decision the action rests on was a DENY. |
DEPENDENCY_TRANSACTION_MISMATCH | D5 | The prior decisions the action names do not all belong to the same transaction. |
DEPENDENCY_AUTHORITY_EXPIRED | D6 | For a CURRENT_AUTHORITY dependency: the Grant behind the prior decision has expired since. |
DEPENDENCY_ISSUER_NOT_TRUSTED | D7 | For a CURRENT_AUTHORITY dependency: the company behind the prior decision has since been marked suspended or disputed. |
DEPENDENCY_ISSUER_KEY_COMPROMISED | D7 | For a CURRENT_AUTHORITY dependency: the key that signed the prior decision's Grant has since been marked compromised. |
DEPENDENCY_AUTHORITY_REVOKED | D7 | For a CURRENT_AUTHORITY dependency: the Grant behind the prior decision has been revoked since. |
Notes on these rows:
ISSUER_andNOT_ TRUSTED ISSUER_are both T1. UnderUNKNOWN delegus-, T1 passes when the issuer is a registered Principal in good standing. A company that is not registered at all still getstrust- v3 ISSUER_; a registered one that is suspended or disputed getsUNKNOWN ISSUER_. When standing is restored, the same request is checked normally again. Receipts already signed do not change. For relying parties onNOT_ TRUSTED delegus-, standing is not checked andbase- v1 ISSUER_stays reserved, as on the current page. Spec §5.3: T1 isNOT_ TRUSTED issueris a registered Principal in good standing. Staff mark a Principalsuspendedordisputedon a misuse report, with a reason, audited; a Grant from such a Principal gets a signed DENYISSUER_. UnderNOT_ TRUSTED delegus-only; receipts already issued are never altered.base- v3 - Tell the two
AUTHORITY_cases apart by which check isEXHAUSTED false:protocol.orchecks. P21 trust.. Both come after T5, so the agent needs a fresh Proof to try again.checks. T7 - Dependency checks run for each declared entry, in the order the action lists them. D1 runs once, first. Each of D1–D7 is
falseif any entry fails it, andreasonis the firstfalsecheck in the order above, not the first failing entry. For aPRIOR_entry, D6 and D7 areDECISION "skipped". - Dependency checks run after P21. A dependency failure comes after T5, so the Proof is used up; the agent needs a fresh one to try again.
SERVICE_is also the answer at D2 when the stored dependency record is unavailable or malformed, and at D7 when the dependency's revocation state is missing (spec §5.2, the dependency table).UNAVAILABLE
Prerequisites added by v0.3#
A check whose prerequisite did not pass is reported "skipped". v0.3 adds three rules (§5.4):
| Check | Needs |
|---|---|
| P21 | Every earlier check true: P1–P20 and T1, T2, T3, T5 |
| D1–D7 | P21 true (P21 is true without any read when the matched Grant has no budget) |
| T7 | Every other check true |
One consequence: when a trust check fails (for example PROOF_ at T5), a v0.3 receipt shows P21 and D1–D7 as "skipped" and protocol., even though no P-check is false. Under v0.2 the same replay shows protocol.. The decision and reason are the same in both. Always read the reason from reason, never from protocol..
HTTP refusals, no receipt#
On /verify#
These rows extend the existing "Refusals without a receipt" table.
| Code | HTTP | When | Profiles |
|---|---|---|---|
RP_SUSPENDED | 403 | The relying party making the call is not in good standing: Delegus staff have suspended it or marked it disputed. | every profile |
RP_UNVERIFIED | 403 | The relying party is on its own domain (not a Delegus-managed identity), and its organization does not hold a verified claim on that domain. Verify the domain claim first. | every profile |
REQUIREMENTS_MALFORMED | 400 | The request's requires list is not well formed: it must have 1 to 8 distinct entries, each exactly { "fingerprint", "mode" }, with mode either PRIOR_DECISION or CURRENT_AUTHORITY. The Proof is not used up. | any (the body is checked first) |
REQUIREMENTS_NOT_SUPPORTED | 400 | The request carries requires, but the relying party is not on delegus-base-v3. | any other than delegus-base-v3 |
RECEIPT_TOO_LARGE | 422 | Already on the page today. v0.3 makes it part of the specification (§6.4) and adds: no budget is spent, and the size is measured for the complete receipt before anything is written. | every profile |
Notes:
- Order on
/verify: the API key, then the request body (REQUIREMENTS_), then the relying party (MALFORMED RP_,SUSPENDED RP_), thenUNVERIFIED REQUIREMENTS_, then evaluation.NOT_ SUPPORTED RECEIPT_is decided after evaluation, before anything is written.TOO_ LARGE RECEIPT_is not returned when the result would be aTOO_ LARGE PROOF_orREPLAYED SERVICE_receipt; those receipts are small and are returned instead (§6.4). The engine's message suggests narrowing the Grant's matching Capability.UNAVAILABLE - Spec §5.1 names
RP_andSUSPENDED RP_as service gates before evaluation, for every profile: the relying party's standing is not good, or it is customer-hosted and its organization holds no verified claim on its domain (a claim that lapses turns the refusal on until it is re-verified). HTTP 403, no receipt, the Proof'sUNVERIFIED jtinot consumed. Registering a customer-hosted relying party or Principal without a verified claim is refused withCLAIM_.REQUIRED
When claiming a company domain#
A company proves it controls a domain before Delegus Check trusts a company identity or a relying party on that domain. These codes come from starting a domain claim (through the console). Each is an HTTP error with no receipt.
| Code | HTTP | When |
|---|---|---|
TERMS_REQUIRED | 400 | The claim does not record acceptance of the acceptable-use terms as exactly { accepted_by, accepted_at, version }. |
CLAIM_DOMAIN_INVALID | 400 | The domain is not a valid DNS hostname, is managed by Delegus itself, or is a public suffix (such as co.uk) that no one can claim. |
CLAIM_FREE_EMAIL | 403 | The person starting the claim signed in with a free email provider. Only a company email can claim a domain. |
CLAIM_EMAIL_MISMATCH | 403 | The person's email is not on the claimed domain or a parent of it within the same registrable domain. |
CLAIM_DOMAIN_VERIFIED | 409 | Another organization has already verified this domain. |
CLAIM_REQUIRED | 403 | Registering a company identity or a relying party on a domain the organization has not claimed. Claim the domain first. |
The 403 claim errors carry a terms_url member pointing to delegus.ai/terms#acceptable-use.
These come from the onboarding routes (/), not from a decision; CLAIM_ is in spec §5.1. They are listed here so every code a caller can meet is in one place.
Not a Delegus reason#
PROOF_ is not a reason code and never appears in a receipt. It is the SDK's ProofBindingError, raised on the relying party's side before /verify is called, when the Proof's htm/htu do not match the request the service actually received. Handle it as a client-side rejection; there is no receipt for it.
Reading a DENY#
A DENY is a signed receipt like any ALLOW (§6). It carries both protocol. and trust., and the per-check outcomes, so you can see which checks passed before the first failure and which were skipped after it. When the DENY happens before P3 passes, the receipt omits principal, agent, grant_id, authority and expires_at (§6.4), because nothing about the Grant has been verified yet. Codes that begin with GRANT_ or ISSUER_ are about the company and its Grant; PROOF_ codes are about the agent's request; ACTION_, AMOUNT_, CURRENCY_, RESOURCE_ and UNKNOWN_ are about whether this action fits the granted authority.