The consent receipt
The receipt is the citizen-facing, independently verifiable artifact. It is a
W3C Verifiable Credential that carries the consented items and both proofs:
the company's reused companySig and the citizen's WebAuthn assertion over
Agreely's keccak challenge.
It deliberately never carries the salt, the consentRef, or the commitment.
Those stay in the shreddable envelope so the receipt does not undo
unlinkability or block erasure. Like
the offer, it is number-free: every value is a string.
{
"@context": [
"https://www.w3.org/ns/credentials/v2",
"https://agreely.ca/credentials/consent/v1"
],
"type": ["VerifiableCredential", "ConsentReceipt"],
"id": "urn:agreely:receipt:6f9619ff-8b86-d011-b42d-00cf4fc964ff",
"issuer": "did:web:agreely.ca:c:acme",
"validFrom": "2026-06-26T15:04:05Z",
"validUntil": "2031-01-01T00:00:00Z",
"credentialSubject": {
"id": "did:agreely:citizen:9F8K2M4P7Q1R3T5V8W0X2Y4Z6B",
"consent": {
"action": "grant",
"assuranceLevel": "citizen_signed",
"items": [
{
"itemId": "0x9a2b3c4d5e6f70819a2b3c4d5e6f70819a2b3c4d5e6f70819a2b3c4d5e6f7081",
"category": "phone number",
"purpose": "billing"
}
],
"requestId": "0x4b08245219c0e1a2b3c4d5e6f70819a2b3c4d5e6f70819a2b3c4d5e6f7081234",
"grantedAt": "2026-06-26T15:04:05Z",
"document": {
"code": "acme-billing-v3",
"name": "Billing consent v3",
"version": "3",
"effectiveDate": "2026-01-01",
"ipfsCid": "bafybeib..."
},
"disclosureHash": "0x…sha256…"
}
},
"proof": [
{
"type": "DataIntegrityProof",
"cryptosuite": "eddsa-jcs-2022",
"created": "2026-06-26T15:00:00Z",
"verificationMethod": "did:web:agreely.ca:c:acme#kms-1",
"proofPurpose": "assertionMethod",
"proofValue": "z3Ge8wQ5...base58btc..."
},
{
"type": "WebAuthnAssertion",
"verificationMethod": "did:agreely:citizen:9F8K2M4P7Q1R3T5V8W0X2Y4Z6B#passkey-1",
"nonce": "0x7081234b3c4d5e6f70819a2b3c4d5e6f70819a2b3c4d5e6f70819a2b3c4d5e6f",
"challenge": "0xc0ffee...keccak256...",
"authenticatorData": "SZYN5YgOjGh0NBcP...base64url...",
"clientDataJSON": "eyJ0eXBlIjoid2ViYXV0aG4uZ2V0Iiw...base64url...",
"signature": "MEUCIQD...base64url..."
}
]
}
Field by field
| Field | Meaning |
|---|---|
@context |
The VC v2 context plus Agreely's consent credential context. |
type |
["VerifiableCredential", "ConsentReceipt"]. |
id |
urn:agreely:receipt:{uuid}. |
issuer |
The company DID (carried over from the offer's issuer). |
validFrom |
grantedAt, when the citizen approved. |
validUntil |
The offer's validUntil, the consent lifespan. |
credentialSubject.id |
The citizen DID, did:agreely:citizen:.... |
credentialSubject.consent.action |
grant. |
credentialSubject.consent.assuranceLevel |
The public assurance level: citizen_signed for this receipt, company_attested for the company-attested variant (see below). |
credentialSubject.consent.items |
The consented items (itemId, normalized category, normalized purpose). |
credentialSubject.consent.requestId |
The originating offer's requestId. |
credentialSubject.consent.grantedAt |
When consent was given (RFC 3339). |
credentialSubject.consent.document |
The document descriptor: code, name, version, effectiveDate, and, when the disclosure version is pinned to IPFS, the ipfsCid of the immutable copy the consent was made under (see IPFS document integrity). The ipfsCid is omitted when the version is not pinned. |
credentialSubject.consent.disclosureHash |
0x + the SHA-256 of the canonicalized disclosure map. |
Two issuers, do not conflate them
The issuer is the company DID (did:web:agreely.ca:c:{slug}); the credentialSubject.id is the citizen DID (did:agreely:citizen:...). They use different DID methods on purpose. See DID documents.
The disclosure binding
consent.document binds the receipt to the exact disclosure version the consent
was made under, by its code, version, and, when pinned, its ipfsCid on IPFS. consent.disclosureHash
is 0x + the SHA-256 of the canonicalized (JCS) disclosure map.
disclosureHash is SHA-256, not keccak256
Be precise about the algorithm: disclosureHash is SHA-256, while the per-cell commitments, the Merkle tree, and the passkey challenge are keccak256. Do not mix the two when you recompute.
The §8 disclosure covers the purposes (fins), the means of
collection, the categories, the retention, consent withdrawal, the recipients,
cross-border communication outside Quebec, rights, the person in charge
(responsable), and any automated decision. Its public, fetchable copy at the
ipfsCid is covered in
IPFS document integrity.
The proof array
proof is an array of exactly two proofs, and the order is fixed.
proof[0]: the company half
The eddsa-jcs-2022 Data Integrity proof, reused verbatim from the offer's
companySig. Its verificationMethod resolves in the company DID document; it
signs the original offer (see
the consent request).
proof[1]: the citizen half
A WebAuthnAssertion over Agreely's challenge.
| Field | Meaning |
|---|---|
type |
WebAuthnAssertion. |
verificationMethod |
The citizen DID's passkey fragment, e.g. ...#passkey-1. |
nonce |
The offer's single-use nonce. |
challenge |
0x-hex `keccak256( JCS(context) |
authenticatorData |
Raw WebAuthn authenticator data, base64url. |
clientDataJSON |
Raw WebAuthn client data, base64url. Its embedded challenge equals the challenge field above. |
signature |
The authenticator's signature, base64url. |
Single-cell erase invalidates the shared signature
Because the company signed over the whole item set, erasing one item from a multi-item receipt invalidates companySig over the surviving set: there is no selective-disclosure proof in v1. Survivors stay independently verifiable via their commitment + Merkle root + on-chain anchor, not via the redacted receipt. SD-JWT / BBS+ selective disclosure is the deferred fix.
The company-attested variant
Not every consent goes through a passkey. When a person hand-signed a paper form, the company issues a company-attested receipt (the manual, or offline, flow). It is built like a citizen receipt, with three differences:
- its
typealso carries"CompanyAttestedConsentReceipt"; consent.assuranceLevelis"company_attested"and is never omitted;- its
credentialSubjecthas no citizenid(there is no citizen DID to bind), and it carries anevidenceblock; - its
proofis a one-element array: the company half only, with no WebAuthn assertion.
{
"@context": [
"https://www.w3.org/ns/credentials/v2",
"https://agreely.ca/credentials/consent/v1"
],
"type": ["VerifiableCredential", "ConsentReceipt", "CompanyAttestedConsentReceipt"],
"id": "urn:agreely:receipt:6f9619ff-8b86-d011-b42d-00cf4fc964ff",
"issuer": "did:web:agreely.ca:c:acme",
"validFrom": "2026-06-26T15:04:05Z",
"validUntil": "2031-01-01T00:00:00Z",
"credentialSubject": {
"consent": {
"action": "grant",
"assuranceLevel": "company_attested",
"items": [
{
"itemId": "0x9a2b3c4d5e6f70819a2b3c4d5e6f70819a2b3c4d5e6f70819a2b3c4d5e6f7081",
"category": "phone number",
"purpose": "billing"
}
],
"requestId": "0x4b08245219c0e1a2b3c4d5e6f70819a2b3c4d5e6f70819a2b3c4d5e6f7081234",
"grantedAt": "2026-06-26T15:04:05Z",
"document": {
"code": "acme-billing-v3",
"name": "Billing consent v3",
"version": "3",
"effectiveDate": "2026-01-01",
"ipfsCid": "bafybeib..."
},
"disclosureHash": "0x…sha256…"
},
"evidence": {
"type": "HandSignedPaperConsent",
"pdfHash": "0x…sha256…",
"pdfHeldBy": "company",
"documentVersion": "3"
}
},
"proof": [
{
"type": "DataIntegrityProof",
"cryptosuite": "eddsa-jcs-2022",
"created": "2026-06-26T15:00:00Z",
"verificationMethod": "did:web:agreely.ca:c:acme#kms-1",
"proofPurpose": "assertionMethod",
"proofValue": "z3Ge8wQ5...base58btc..."
}
]
}
| Field | Meaning |
|---|---|
evidence.type |
HandSignedPaperConsent. |
evidence.pdfHash |
0x + the SHA-256 of the hand-signed PDF. |
evidence.pdfHeldBy |
"company" or "agreely-escrow", who holds the PDF. |
evidence.documentVersion |
The document version the form was signed under. |
What an attested receipt proves, and what it does not
A company-attested receipt proves the company attested that it holds a hand-signed PDF whose hash matches evidence.pdfHash. It does not prove a human signed: there is no passkey assertion, only the company signature. It is a company attestation, not cryptographic proof of the citizen's presence.
Two assurance vocabularies
There are two vocabularies, and they do not mix:
- The internal storage tier is
full|manual. - The public label on the receipt and on
/v1/checkiscitizen_signed|company_attested.
manual maps to company_attested; full maps to citizen_signed. Map them,
never mix the two vocabularies.
Next
- Verify a receipt: the four checks that confirm this document end to end.