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: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 type also carries "CompanyAttestedConsentReceipt";
  • it carries, at the root of the receipt, assuranceLevel: "company_attested", never omitted, and an evidence block that describes the signed sheet;
  • its credentialSubject has no citizen id (there is no citizen DID to bind);
  • its consent carries neither requestId nor disclosureHash, 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, in consent, 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 proof is 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 type carries "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/check returns;
  • its evidence block, also at the root, is of type VerbalConsentDocumentedByOrganisation and names the scriptVersion read and the documentVersion; there is no PDF digest, because there is no PDF;
  • its consent carries channel: "telephone", the documentVersion, the grantedAt (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 proof carries 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