FR EN

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 type also carries "CompanyAttestedConsentReceipt";
  • consent.assuranceLevel is "company_attested" and is never omitted;
  • its credentialSubject has no citizen id (there is no citizen DID to bind), and it carries an evidence block;
  • its proof is 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/check is citizen_signed | company_attested.

manual maps to company_attested; full maps to citizen_signed. Map them, never mix the two vocabularies.

Next