Consent receipts provide cryptographically signed records of user privacy choices, including exactly which purposes and data categories were consented to, when consent was given, and by whom. This creates irrefutable audit trails for regulators while demonstrating good faith compliance to users. Developers should implement receipts as hashed records stored in customer accounts with asymmetric signature verification, ensuring neither customers nor companies can later alter consent history.
Why Consent Receipts Matter
Under regulations like GDPR and CCPA, you must be able to demonstrate that users consented to data processing. A consent receipt serves as that proof. It contains the consent choice, timestamp, version of the privacy notice presented, and a unique identifier that allows verification later.
For developers, implementing consent receipts means building a data model that captures granular consent choices, storing them securely, and providing ways for users to retrieve their consent history.
Data Model for Consent Receipts
The foundation of any consent receipt system is a well-structured data model. Here’s a practical example:
from dataclasses import dataclass
from datetime import datetime
from enum import Enum
import uuid
import hashlib
import json
class ConsentType(Enum):
MARKETING_EMAILS = "marketing_emails"
PERSONALIZED_ADS = "personalized_ads"
ANALYTICS = "analytics"
THIRD_PARTY_SHARING = "third_party_sharing"
DATA_RETENTION = "data_retention"
class ConsentAction(Enum):
GRANTED = "granted"
REVOKED = "revoked"
UPDATED = "updated"
@dataclass
class ConsentReceipt:
receipt_id: str
user_id: str
consent_type: ConsentType
action: ConsentAction
timestamp: datetime
privacy_notice_version: str
ip_address: str
user_agent: str
purpose: str
data_controller: str
def to_json(self):
return json.dumps({
"receipt_id": self.receipt_id,
"user_id": hash_user_id(self.user_id), # Hash for privacy
"consent_type": self.consent_type.value,
"action": self.action.value,
"timestamp": self.timestamp.isoformat(),
"privacy_notice_version": self.privacy_notice_version,
"purpose": self.purpose,
"data_controller": self.data_controller,
"verification_hash": self.generate_hash()
}, indent=2)
def generate_hash(self):
data = f"{self.receipt_id}{self.user_id}{self.timestamp.isoformat()}"
return hashlib.sha256(data.encode()).hexdigest()[:16]
def hash_user_id(user_id: str) -> str:
return hashlib.sha256(user_id.encode()).hexdigest()[:12]
This model captures the essential elements: what was consented, when, and under what version of your privacy notice. The verification hash allows you to prove the receipt hasn’t been tampered with.
Storing Consent Receipts
For production systems, you need durable storage that supports both quick retrieval and audit requirements. Consider this PostgreSQL schema:
CREATE TABLE consent_receipts (
receipt_id UUID PRIMARY KEY,
user_hash VARCHAR(12) NOT NULL,
consent_type VARCHAR(50) NOT NULL,
action VARCHAR(20) NOT NULL,
timestamp TIMESTAMPTZ NOT NULL,
privacy_notice_version VARCHAR(20) NOT NULL,
purpose TEXT NOT NULL,
data_controller VARCHAR(255) NOT NULL,
verification_hash VARCHAR(16) NOT NULL,
created_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE INDEX idx_user_hash ON consent_receipts(user_hash);
CREATE INDEX idx_timestamp ON consent_receipts(timestamp);
-- For GDPR right to access, retrieve all receipts for a user
CREATE TABLE user_consent_map (
user_id_encrypted VARCHAR(255) PRIMARY KEY,
user_hash VARCHAR(12) NOT NULL
);
This schema separates the encrypted user identifier from the pseudonymous hash, adding an extra layer of privacy while maintaining the ability to retrieve consent records.
Capturing Consent in Your Application
When a user updates their privacy preferences, you need to capture consent at the point of choice. Here’s a Flask example:
from flask import Flask, request, jsonify
from datetime import datetime
import uuid
app = Flask(__name__)
@app.route('/api/consent', methods=['POST'])
def update_consent():
data = request.get_json()
# Validate required fields
required = ['user_id', 'consents', 'privacy_version']
if not all(k in data for k in required):
return jsonify({"error": "Missing required fields"}), 400
receipts = []
for consent_type, granted in data['consents'].items():
receipt = ConsentReceipt(
receipt_id=str(uuid.uuid4()),
user_id=data['user_id'],
consent_type=ConsentType(consent_type),
action=ConsentAction.GRANTED if granted else ConsentAction.REVOKED,
timestamp=datetime.utcnow(),
privacy_notice_version=data['privacy_version'],
ip_address=request.remote_addr,
user_agent=request.headers.get('User-Agent', ''),
purpose=get_purpose_for_consent(consent_type),
data_controller="Your Company Name"
)
save_receipt(receipt)
receipts.append(receipt.to_json())
return jsonify({
"status": "success",
"receipts": receipts
}), 200
def get_purpose_for_consent(consent_type: str) -> str:
purposes = {
"marketing_emails": "Send promotional emails about products and services",
"personalized_ads": "Deliver targeted advertisements based on interests",
"analytics": "Analyze usage patterns to improve our services",
"third_party_sharing": "Share data with trusted partners",
"data_retention": "Retain data for specified periods"
}
return purposes.get(consent_type, "Unknown purpose")
This endpoint accepts a user’s consent preferences and generates a receipt for each choice. The response includes the full receipt data so users can immediately verify what was recorded.
Building the User Consent Dashboard
Users need a way to view their consent history. Build a simple retrieval endpoint:
@app.route('/api/consent/history', methods=['GET'])
def get_consent_history():
user_id = request.headers.get('X-User-Id')
if not user_id:
return jsonify({"error": "Authentication required"}), 401
user_hash = hash_user_id(user_id)
receipts = fetch_receipts_by_hash(user_hash)
return jsonify({
"user_consents": receipts,
"last_updated": max(r['timestamp'] for r in receipts) if receipts else None
}), 200
Pair this with a frontend that displays each consent type, its current status, and the timestamp of the last change. Allow users to export their full consent history as JSON or PDF for their records.
Verifying Consent Receipts
For audit purposes or regulatory requests, you need to verify a receipt’s authenticity:
def verify_receipt(receipt_json: dict) -> bool:
stored_hash = receipt_json.get('verification_hash')
# Recalculate hash
expected_hash = hashlib.sha256(
f"{receipt_json['receipt_id']}{receipt_json['user_id']}{receipt_json['timestamp']}".encode()
).hexdigest()[:16]
return stored_hash == expected_hash
This verification ensures the receipt hasn’t been modified after generation. Store the original receipt data immutably—append-only logs work well for this.
Practical Considerations
When implementing consent receipts, keep these points in mind:
First, version your privacy notices. Every receipt should reference the specific version the user saw when making their choice. When you update your privacy policy, increment the version number so you can prove which version applied to each consent.
Second, implement consent refresh. For ongoing processing activities, periodically re-confirm consent to maintain valid legal basis. GDPR recommends refreshing consent every two years for legitimate interests.
Third, handle cross-border transfers. If your data processing involves international transfers, capture the transfer mechanism (SCCs, BCRs, etc.) in the receipt so you can demonstrate compliance.
Fourth, prepare for data subject requests. Your consent receipt system should integrate with your response to GDPR access requests or CCPA consumer rights requests. The ability to export a user’s full consent history in a standard format is essential.
Related Articles
- Implement Data Portability Feature For Customers Gdpr Right
- Zero Knowledge Proof Messaging How Future Protocols Will Pro
- How To Build Privacy Dashboard For Customers To Manage Their
- How To Create Tiered Access Plan Giving Executor Immediate A
- How To Safely Share Location With Date Without Giving Perman
Built by theluckystrike — More at zovo.one