The consent lifecycle
A consent in Agreely moves through five stages, each a distinct artifact. The
company issues an offer, the person approves it with a passkey, that answer
becomes a receipt, the receipt updates a single enforcement record, and your code
reads that record on every use. The whole point is that each stage leaves proof,
so the final check is not a bare boolean but the tip of a verifiable chain.
1. Offer
The company issues a signed consent request for a set of catalog cells,
delivered to the person as a secure deep link. The offer is signed by the
company's DID key and commits to a Merkle root over the exact
(category, purpose) cells being requested, so the person approves precisely
what was offered and nothing can be swapped in afterward.
2. Passkey approval
The person approves per-cell in the citizen portal with a WebAuthn passkey. The passkey signs a challenge bound to the offer's Merkle root, which is what ties a specific human's authenticator to a specific set of cells at a specific moment. Approval is granular: a person can approve some cells of an offer and decline others.
The passkey signs the root, not the plaintext
The challenge the passkey signs is bound to the offer's Merkle root. That is enough to prove the person approved exactly those cells, and it means the citizen side never has to hold the company's plaintext customer identifier to produce a valid approval.
3. ConsentReceipt
The approval becomes a ConsentReceipt, a W3C Verifiable Credential that carries both the company signature and the citizen's passkey assertion. This is the portable, independently verifiable record of what happened. Anyone holding it can verify the receipt without trusting Agreely.
A manual or offline consent, where a person signs on paper rather than in the portal, instead yields a company-attested receipt. It carries the company signature alone: the company attests that it holds a hand-signed document whose hash matches. It does not carry a passkey assertion, and it does not prove a human signed. See assurance tiers below.
4. Enforcement record
The receipt updates a single authoritative enforcement record, keyed per cell
and carrying a consent_ref, a status, and an assurance_tier. This is the row
a check reads. It is keyed by the company's own plaintext customer_id, and no citizen
DID is ever written here. That separation is what keeps the company side and the
citizen side from being trivially joined; see
unlinkability.
5. The /v1 check
Your code calls the synchronous /v1 check,
a default-deny read of exactly that enforcement status. A withdrawal flips the
record to revoked, and the very next call denies, with no chain wait. The
chain is never read on this path; the enforcement record is authoritative the
instant a grant or withdrawal commits.
Assurance tiers
Two vocabularies describe how a consent was obtained. Map them, do not mix them.
- The storage enum is
fullormanual.fullmeans the consent went through passkey approval in the portal;manualmeans it was collected offline and attested by the company. - The public label, on the receipt and in the
/v1/checkresponse, iscitizen_signedorcompany_attested.fullmaps tocitizen_signed;manualmaps tocompany_attested.
Both tiers enforce identically
A citizen_signed consent carries the person's passkey assertion. A company_attested consent is the company's attestation that it holds a hand-signed document whose hash matches, and it does not prove a human signed. At check time both enforce exactly the same way: only the tier label differs, so a reader can tell how strong the underlying proof is.
Next
- Unlinkability: why the enforcement record and the citizen DID are kept apart.
- Honest boundaries: what a company-attested receipt does and does not prove.
- The protocol for the cryptography underneath.