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:app.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",
"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:app.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.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, nameEn when the company named the document in English, 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. |
credentialSubject.consent.declinedItems |
The items the person declined. Present only when she declined at least one: the receipt then shows every purpose asked, not only the accepted ones. |
credentialSubject.consent.disclosureVersion and legalBasis |
Present only when a cell of the offer rests on a ground other than consent: disclosureVersion is "2", legalBasis maps each itemId to its ground and justification, and disclosureHash is then over {disclosure, legalBasis}. When absent, the digest is over the disclosure alone. |
credentialSubject.consent.minor |
Present only when the person concerned is a minor under 14: the parental-consent basis the offer carried. |
The citizen receipt carries no assuranceLevel field: its tier,
citizen_signed, is read from its type (ConsentReceipt alone) and its two
proofs. The attested and verbal variants always carry it (see below).
Two issuers, do not conflate them
The issuer is the company DID (did:web:app.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 disclosure under P-39.1 s. 8 (A-2.1 s. 65 for a public body) 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, and, for a public body, its name, whether the request is mandatory or optional, and the consequences of refusing or withdrawing (A-2.1 s. 65, first paragraph, subparagraphs 1, 4 and 5). 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 differs from a citizen receipt on these points:
- its
typealso carries"CompanyAttestedConsentReceipt"; - it carries, at the root of the receipt,
assuranceLevel: "company_attested", never omitted, and anevidenceblock that describes the signed sheet; - its
credentialSubjecthas no citizenid(there is no citizen DID to bind); - its
consentcarries neitherrequestIdnordisclosureHash, because no offer was signed online: it names instead the document version (documentVersion) and the date of the signature (effectiveDate); - a recent receipt carries
receiptVersion: "2"and, inconsent,recordedAt, the instant the organization recorded it in Agreely and the company key signed. An older receipt has neither; both verify the same way; - its
proofis a one-element array: the company half only, with no WebAuthn assertion.
{
"receiptVersion": "2",
"@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:app.agreely.ca:c:acme",
"assuranceLevel": "company_attested",
"validFrom": "2026-06-26T15:04:05Z",
"validUntil": "2031-01-01T04:59:59Z",
"credentialSubject": {
"consent": {
"action": "grant",
"items": [
{
"itemId": "0x9a2b3c4d5e6f70819a2b3c4d5e6f70819a2b3c4d5e6f70819a2b3c4d5e6f7081",
"category": "phone number",
"purpose": "billing"
}
],
"documentVersion": "e4d41dc3-c8a5-44b1-96c5-be740c45cc6d",
"document": {
"code": "acme-billing-v3",
"name": "Billing consent v3",
"version": "3",
"effectiveDate": "2026-01-01",
"ipfsCid": "bafybeib..."
},
"effectiveDate": "2026-06-26T15:04:05Z",
"grantedAt": "2026-06-26T15:04:05Z",
"recordedAt": "2026-06-27T13:12:40Z"
}
},
"evidence": {
"type": "HandSignedPaperConsent",
"pdfHash": "0x…sha256…",
"pdfHeldBy": "company",
"documentVersion": "e4d41dc3-c8a5-44b1-96c5-be740c45cc6d"
},
"proof": [
{
"type": "DataIntegrityProof",
"cryptosuite": "eddsa-jcs-2022",
"created": "2026-06-27T13:12:40Z",
"verificationMethod": "did:web:app.agreely.ca:c:acme#kms-1",
"proofPurpose": "assertionMethod",
"proofValue": "z3Ge8wQ5...base58btc..."
}
]
}
| Field | Meaning |
|---|---|
receiptVersion |
"2" on a recent receipt; absent on a receipt of the first shape. |
assuranceLevel |
"company_attested", always present, at the root of the receipt. |
credentialSubject.consent.documentVersion |
The id of the document version the sheet was signed under. |
credentialSubject.consent.effectiveDate |
The date the sheet was signed. |
credentialSubject.consent.grantedAt |
The consent date. It equals effectiveDate, except when the sheet confirms a consent given by telephone: the consent then dates from the call, and consent.confirms names the confirmed verbal consent (assuranceLevel, merkleRoot, grantedAt). |
credentialSubject.consent.recordedAt |
The instant it was recorded in Agreely and the company signed, covered by the signature. The proof's created is informative: the signature is over the receipt body, not over the proof options. |
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 id of the document version, the same as consent.documentVersion. |
evidence.documentVersionMatch |
Present only when the sheet was signed before this version was published in Agreely: "attested-by-issuer", the organization attests the sheet carried this version's text. The value names who attests; it is not a check by Agreely. |
What an attested receipt proves, and what it does not
A company-attested receipt documents that the company attested 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.
The verbal variant
A consent given by telephone yields a third receipt, weaker still, which says so on the artifact itself:
- its
typecarries"VerbalConsentReceipt", never"CompanyAttestedConsentReceipt": a verifier that reads only the type must not believe a signed sheet exists; - it carries, at the root of the receipt,
assuranceLevel: "company_documented", the same value/v1/checkreturns; - its
evidenceblock, also at the root, is of typeVerbalConsentDocumentedByOrganisationand names thescriptVersionread and thedocumentVersion; there is no PDF digest, because there is no PDF; - its
consentcarrieschannel: "telephone", thedocumentVersion, thegrantedAt(the instant of the call) and, on a recent receipt,recordedAt; - the purposes the person answered "no" appear in
declinedItems, so the receipt shows every question put; - its
proofcarries the company half only.
It records that the organization stated, at a recorded instant (the receipt's digest is then anchored), that the person consented by telephone to these purposes under this script. It does not prove the call happened. The name of the staff member who took the call is not in it.
The assurance vocabularies
There are two vocabularies, and they do not mix:
| Storage tier | Public label (receipt and /v1/check) |
|---|---|
full |
citizen_signed |
manual |
company_attested |
verbal |
company_documented |
/v1/check returns both, as tier and assurance. Map them, never mix the two
vocabularies.
Next
- Verify a receipt: the four checks that confirm this document end to end.