TLDR: The IAB's entire legitimate interest framework for advertising got invalidated. Courts have ruled: tracking and personalized ads require consent, not "legitimate interest." The loophole is closed.
Read full summary
Deep dive into GDPR's legitimate interest basis after recent court rulings and regulatory guidance. Covers the three-part test, documentation requirements, examples of valid (and invalid) uses, and when consent is still required. Includes complete TypeScript implementations for Legitimate Interest Assessments (LIA), automated compliance checking, and practical guidance for marketing, security, and analytics use cases.
*Summary by Claude AI*
## The Loophole That Got Slammed Shut
For years, AdTech companies had a convenient workaround. GDPR requires consent for tracking? Just claim "legitimate interest" instead. The IAB's Transparency and Consent Framework (TCF) even built this in—vendors could assert legitimate interest for purposes like personalized advertising, bypassing user consent entirely.
In February 2022, Belgium's Data Protection Authority demolished that framework. They ruled that legitimate interest claims for personalized advertising were invalid. The TC String itself—the mechanism encoding user consent—was declared personal data. IAB Europe was fined €250,000 and given months to rebuild the entire system.
The case is still working through appeals, but the regulatory direction is clear. The CJEU's Meta ruling reinforced it: when a platform has market dominance, users don't have genuine alternatives, making "consent" questionable and legitimate interest claims even weaker. The French CNIL's ongoing enforcement confirms it: legitimate interest is not a magic bypass for activities that should require consent.
If your data processing relies on legitimate interest for advertising, analytics, or tracking, you're building on a foundation regulators are actively demolishing.
## What Legitimate Interest Actually Requires
Legitimate interest under GDPR Article 6(1)(f) isn't a checkbox. It's a three-part test that requires documented analysis, ongoing review, and genuine balancing of interests. Most organizations claiming legitimate interest haven't done the required work.
## The Legal Framework: Article 6(1)(f) Explained
GDPR Article 6(1)(f) states that processing is lawful when it's "necessary for the purposes of the legitimate interests pursued by the controller or by a third party, except where such interests are overridden by the interests or fundamental rights and freedoms of the data subject which require protection of personal data, in particular where the data subject is a child."
Let's break down the key elements:
| Element | What It Means | Key Questions |
|---------|---------------|---------------|
| **Legitimate interest** | A real, current interest that is lawful | Is this interest recognized by law? Is it articulated specifically enough? |
| **Necessity** | Processing must be required for that interest | Could the same purpose be achieved with less data or without processing? |
| **Balancing test** | Data subject rights must not override | What impact does processing have on individuals? What are their expectations? |
## Recent Regulatory and Court Developments
The interpretation of legitimate interest has evolved significantly through enforcement actions and court rulings:
### Key CJEU Rulings
```typescript
interface LegitimateInterestCaseLaw {
case: string;
ruling: string;
implications: string[];
}
const recentCaseLaw: LegitimateInterestCaseLaw[] = [
{
case: 'Meta v Bundeskartellamt (C-252/21)',
ruling: 'Legitimate interest cannot justify combining data from different services without consent',
implications: [
'Cross-service data combination requires explicit consent',
'Market dominance affects the balancing test',
'Users must have genuine choice'
]
},
{
case: 'CNIL v Google (2019/2022)',
ruling: 'Pre-ticked boxes and bundled consent do not constitute valid consent; legitimate interest not available for advertising tracking',
implications: [
'Advertising tracking requires consent, not legitimate interest',
'Transparency requirements are strict',
'Granular consent options required'
]
},
{
case: 'IAB Europe TCF (Belgian DPA, 2022)',
ruling: 'TCF legitimate interest claims for personalized advertising are invalid',
implications: [
'Personalized advertising cannot rely on legitimate interest',
'TC String is personal data',
'Industry frameworks must be GDPR-compliant'
]
},
{
case: 'Rigas (C-13/16)',
ruling: 'Legitimate interest requires a "sufficiently detailed" balancing test',
implications: [
'Balancing tests must be documented',
'Generic assessments are insufficient',
'Case-by-case analysis required'
]
}
];
```
### Regulatory Guidance Evolution
The Article 29 Working Party (now EDPB) Opinion 06/2014 remains authoritative, but subsequent guidance has clarified:
1. **Legitimate interest is not a fallback**: Organizations cannot switch to legitimate interest when consent is refused
2. **Higher bar for sensitive processing**: Any processing that could significantly affect individuals requires stronger justification
3. **Online tracking**: Most tracking requires consent, not legitimate interest
4. **Direct marketing**: While B2B marketing may qualify, B2C marketing increasingly requires consent
## The Three-Part Test: A Deep Dive
Every legitimate interest assessment must address three questions in sequence. If any step fails, you cannot rely on legitimate interest.
### Building a Legitimate Interest Assessment System
```typescript
interface LegitimateInterestAssessment {
id: string;
processingActivity: string;
dataController: string;
assessmentDate: Date;
reviewer: string;
status: 'draft' | 'approved' | 'rejected' | 'review_needed';
// The three-part test
purposeTest: PurposeTest;
necessityTest: NecessityTest;
balancingTest: BalancingTest;
// Final outcome
outcome: AssessmentOutcome;
safeguards: Safeguard[];
reviewSchedule: Date;
}
interface PurposeTest {
legitimateInterest: string;
interestHolder: 'controller' | 'third_party' | 'data_subject';
legalBasis: string;
// Validation criteria
isSpecific: boolean;
isReal: boolean; // Not speculative
isCurrent: boolean; // Not hypothetical future use
isLawful: boolean; // Not prohibited by law
isArticulated: boolean; // Clearly defined
evidence: string[];
notes: string;
}
interface NecessityTest {
processingDescription: string;
dataCategories: string[];
dataVolume: 'minimal' | 'moderate' | 'extensive';
retentionPeriod: string;
// Necessity analysis
alternativesConsidered: Alternative[];
chosenApproach: string;
justification: string;
// Key questions
isProcessingNecessary: boolean;
couldUseAnonymizedData: boolean;
couldUseLessData: boolean;
isRetentionProportionate: boolean;
}
interface Alternative {
description: string;
whyNotChosen: string;
privacyImpact: 'lower' | 'same' | 'higher';
}
interface BalancingTest {
// Nature of the data
dataSensitivity: 'standard' | 'special_category' | 'quasi_sensitive';
dataSubjects: DataSubjectProfile;
// Impact assessment
impactOnDataSubjects: ImpactAssessment;
// Reasonable expectations
relationshipWithDataSubject: string;
wasDataCollectedDirectly: boolean;
whatWasDataSubjectTold: string;
wouldProcessingBeSurprising: boolean;
// Safeguards that tip the balance
safeguardsInPlace: string[];
optOutAvailable: boolean;
optOutMechanism: string;
// Final balance
controllerInterestStrength: 'weak' | 'moderate' | 'strong' | 'compelling';
dataSubjectImpact: 'minimal' | 'moderate' | 'significant' | 'severe';
balanceOutcome: 'controller_prevails' | 'data_subject_prevails' | 'uncertain';
}
interface DataSubjectProfile {
categories: string[]; // e.g., 'customers', 'website visitors', 'employees'
includesChildren: boolean;
includesVulnerable: boolean;
approximateNumber: number;
}
interface ImpactAssessment {
physicalHarm: RiskLevel;
financialHarm: RiskLevel;
reputationalHarm: RiskLevel;
emotionalDistress: RiskLevel;
lossOfControl: RiskLevel;
discriminationRisk: RiskLevel;
mitigatingFactors: string[];
aggravatingFactors: string[];
}
type RiskLevel = 'none' | 'low' | 'medium' | 'high' | 'severe';
interface AssessmentOutcome {
canRelyOnLegitimateInterest: boolean;
confidenceLevel: 'high' | 'medium' | 'low';
reasoning: string;
conditions: string[];
alternativeBaseRecommended?: string;
}
interface Safeguard {
type: 'technical' | 'organizational' | 'contractual';
description: string;
effectiveness: 'high' | 'medium' | 'low';
implementationStatus: 'implemented' | 'planned' | 'not_applicable';
}
class LegitimateInterestAssessmentEngine {
private assessments: Map = new Map();
createAssessment(
processingActivity: string,
dataController: string,
reviewer: string
): LegitimateInterestAssessment {
const assessment: LegitimateInterestAssessment = {
id: this.generateId(),
processingActivity,
dataController,
assessmentDate: new Date(),
reviewer,
status: 'draft',
purposeTest: this.createEmptyPurposeTest(),
necessityTest: this.createEmptyNecessityTest(),
balancingTest: this.createEmptyBalancingTest(),
outcome: {
canRelyOnLegitimateInterest: false,
confidenceLevel: 'low',
reasoning: '',
conditions: []
},
safeguards: [],
reviewSchedule: this.calculateReviewDate()
};
this.assessments.set(assessment.id, assessment);
return assessment;
}
evaluatePurposeTest(assessment: LegitimateInterestAssessment): PurposeTestResult {
const { purposeTest } = assessment;
const issues: string[] = [];
let passes = true;
// Check all criteria
if (!purposeTest.isSpecific) {
issues.push('Interest is not specific enough. Vague interests like "improving our services" are insufficient.');
passes = false;
}
if (!purposeTest.isReal) {
issues.push('Interest appears speculative rather than real and current.');
passes = false;
}
if (!purposeTest.isCurrent) {
issues.push('Interest relates to hypothetical future use rather than current needs.');
passes = false;
}
if (!purposeTest.isLawful) {
issues.push('Interest may conflict with legal obligations or ethical standards.');
passes = false;
}
if (!purposeTest.isArticulated) {
issues.push('Interest needs clearer articulation to assess necessity and balancing.');
passes = false;
}
// Check for recognized legitimate interests
const recognizedInterests = this.getRecognizedInterests();
const isRecognized = recognizedInterests.some(ri =>
purposeTest.legitimateInterest.toLowerCase().includes(ri.keyword.toLowerCase())
);
return {
passes,
issues,
isRecognizedInterest: isRecognized,
recommendations: passes ? [] : ['Consider reformulating the legitimate interest to be more specific and demonstrable.']
};
}
evaluateNecessityTest(assessment: LegitimateInterestAssessment): NecessityTestResult {
const { necessityTest } = assessment;
const issues: string[] = [];
let passes = true;
// Check if less invasive alternatives exist
const lessInvasiveAlternatives = necessityTest.alternativesConsidered.filter(
alt => alt.privacyImpact === 'lower'
);
if (lessInvasiveAlternatives.length > 0) {
const wellJustified = lessInvasiveAlternatives.every(
alt => alt.whyNotChosen && alt.whyNotChosen.length > 50
);
if (!wellJustified) {
issues.push('Less invasive alternatives exist but rejection reasons are not well-documented.');
passes = false;
}
}
// Check data minimization
if (necessityTest.dataVolume === 'extensive' && !necessityTest.isProcessingNecessary) {
issues.push('Extensive data processing without clear necessity justification.');
passes = false;
}
if (necessityTest.couldUseAnonymizedData) {
issues.push('Anonymized data could achieve the same purpose. Personal data processing may not be necessary.');
passes = false;
}
if (necessityTest.couldUseLessData) {
issues.push('Less data could achieve the same purpose. Data minimization principle not satisfied.');
passes = false;
}
if (!necessityTest.isRetentionProportionate) {
issues.push('Retention period appears longer than necessary for the stated purpose.');
passes = false;
}
return {
passes,
issues,
dataMinimizationScore: this.calculateDataMinimizationScore(necessityTest),
recommendations: this.generateNecessityRecommendations(necessityTest, issues)
};
}
evaluateBalancingTest(assessment: LegitimateInterestAssessment): BalancingTestResult {
const { balancingTest, purposeTest } = assessment;
// Calculate scores for each side
const controllerScore = this.calculateControllerInterestScore(purposeTest, balancingTest);
const dataSubjectScore = this.calculateDataSubjectImpactScore(balancingTest);
const issues: string[] = [];
let outcome: 'controller_prevails' | 'data_subject_prevails' | 'uncertain';
// Apply the balancing logic
if (balancingTest.dataSubjects.includesChildren) {
// GDPR explicitly mentions children - significant weight to their interests
dataSubjectScore.weight *= 1.5;
issues.push('Processing involves children - their interests carry additional weight.');
}
if (balancingTest.dataSubjects.includesVulnerable) {
dataSubjectScore.weight *= 1.3;
issues.push('Processing involves vulnerable individuals - additional safeguards may be required.');
}
if (balancingTest.wouldProcessingBeSurprising) {
dataSubjectScore.weight *= 1.2;
issues.push('Processing may be unexpected by data subjects - transparency is crucial.');
}
// Check if opt-out mitigates impact
if (balancingTest.optOutAvailable && balancingTest.optOutMechanism) {
controllerScore.weight *= 1.2;
} else {
issues.push('No opt-out mechanism available - consider implementing one.');
}
// Determine outcome
if (controllerScore.score * controllerScore.weight > dataSubjectScore.score * dataSubjectScore.weight * 1.3) {
outcome = 'controller_prevails';
} else if (dataSubjectScore.score * dataSubjectScore.weight > controllerScore.score * controllerScore.weight) {
outcome = 'data_subject_prevails';
} else {
outcome = 'uncertain';
issues.push('Balancing test result is uncertain - additional safeguards recommended.');
}
return {
passes: outcome === 'controller_prevails',
outcome,
controllerInterestStrength: controllerScore.strength,
dataSubjectImpactLevel: dataSubjectScore.level,
issues,
safeguardsRequired: this.determineSafeguards(balancingTest, outcome),
recommendations: this.generateBalancingRecommendations(outcome, issues)
};
}
finalizeAssessment(assessmentId: string): LegitimateInterestAssessment {
const assessment = this.assessments.get(assessmentId);
if (!assessment) throw new Error('Assessment not found');
const purposeResult = this.evaluatePurposeTest(assessment);
const necessityResult = this.evaluateNecessityTest(assessment);
const balancingResult = this.evaluateBalancingTest(assessment);
// All three tests must pass
const canRely = purposeResult.passes && necessityResult.passes && balancingResult.passes;
// Determine confidence level
let confidence: 'high' | 'medium' | 'low' = 'high';
const totalIssues = [
...purposeResult.issues,
...necessityResult.issues,
...balancingResult.issues
].length;
if (totalIssues > 3) confidence = 'low';
else if (totalIssues > 1) confidence = 'medium';
if (balancingResult.outcome === 'uncertain') confidence = 'low';
assessment.outcome = {
canRelyOnLegitimateInterest: canRely,
confidenceLevel: confidence,
reasoning: this.generateReasoningSummary(purposeResult, necessityResult, balancingResult),
conditions: balancingResult.safeguardsRequired.map(s => s.description),
alternativeBaseRecommended: canRely ? undefined : this.recommendAlternativeBase(assessment)
};
assessment.safeguards = balancingResult.safeguardsRequired;
assessment.status = canRely && confidence !== 'low' ? 'approved' : 'review_needed';
this.assessments.set(assessmentId, assessment);
return assessment;
}
private calculateControllerInterestScore(
purposeTest: PurposeTest,
balancingTest: BalancingTest
): { score: number; weight: number; strength: string } {
let score = 0;
// Recognized interests get higher scores
const recognizedInterests = this.getRecognizedInterests();
for (const ri of recognizedInterests) {
if (purposeTest.legitimateInterest.toLowerCase().includes(ri.keyword.toLowerCase())) {
score += ri.baseScore;
break;
}
}
// Adjust based on stated strength
const strengthMultipliers: Record = {
'weak': 0.5,
'moderate': 1,
'strong': 1.5,
'compelling': 2
};
score *= strengthMultipliers[balancingTest.controllerInterestStrength] || 1;
return {
score,
weight: 1,
strength: balancingTest.controllerInterestStrength
};
}
private calculateDataSubjectImpactScore(
balancingTest: BalancingTest
): { score: number; weight: number; level: string } {
const { impactOnDataSubjects } = balancingTest;
let score = 0;
const riskScores: Record = {
'none': 0,
'low': 1,
'medium': 3,
'high': 5,
'severe': 10
};
// Sum all impact categories
score += riskScores[impactOnDataSubjects.physicalHarm];
score += riskScores[impactOnDataSubjects.financialHarm];
score += riskScores[impactOnDataSubjects.reputationalHarm];
score += riskScores[impactOnDataSubjects.emotionalDistress];
score += riskScores[impactOnDataSubjects.lossOfControl];
score += riskScores[impactOnDataSubjects.discriminationRisk];
// Adjust for mitigating/aggravating factors
score -= impactOnDataSubjects.mitigatingFactors.length * 0.5;
score += impactOnDataSubjects.aggravatingFactors.length * 0.5;
// Adjust based on data sensitivity
if (balancingTest.dataSensitivity === 'special_category') {
score *= 2;
} else if (balancingTest.dataSensitivity === 'quasi_sensitive') {
score *= 1.5;
}
let level: string;
if (score <= 5) level = 'minimal';
else if (score <= 15) level = 'moderate';
else if (score <= 30) level = 'significant';
else level = 'severe';
return { score, weight: 1, level };
}
private determineSafeguards(
balancingTest: BalancingTest,
outcome: string
): Safeguard[] {
const safeguards: Safeguard[] = [];
// Always recommend transparency
safeguards.push({
type: 'organizational',
description: 'Clear privacy notice explaining the legitimate interest processing',
effectiveness: 'high',
implementationStatus: 'planned'
});
// Opt-out mechanism
if (!balancingTest.optOutAvailable) {
safeguards.push({
type: 'technical',
description: 'Implement easy-to-use opt-out mechanism',
effectiveness: 'high',
implementationStatus: 'planned'
});
}
// Data minimization
if (balancingTest.dataSensitivity !== 'standard') {
safeguards.push({
type: 'technical',
description: 'Pseudonymization or encryption of sensitive data elements',
effectiveness: 'high',
implementationStatus: 'planned'
});
}
// Children protection
if (balancingTest.dataSubjects.includesChildren) {
safeguards.push({
type: 'technical',
description: 'Age verification and parental consent mechanisms',
effectiveness: 'high',
implementationStatus: 'planned'
});
}
// Uncertain outcome requires additional safeguards
if (outcome === 'uncertain') {
safeguards.push({
type: 'organizational',
description: 'Regular review and reassessment of processing necessity',
effectiveness: 'medium',
implementationStatus: 'planned'
});
safeguards.push({
type: 'technical',
description: 'Audit logging of all processing activities',
effectiveness: 'medium',
implementationStatus: 'planned'
});
}
return safeguards;
}
private getRecognizedInterests(): Array<{ keyword: string; baseScore: number; description: string }> {
return [
{
keyword: 'fraud prevention',
baseScore: 8,
description: 'Preventing fraud is a recognized legitimate interest with strong support in recitals'
},
{
keyword: 'network security',
baseScore: 8,
description: 'GDPR Recital 49 explicitly recognizes network and information security'
},
{
keyword: 'direct marketing',
baseScore: 4,
description: 'Recital 47 mentions direct marketing but this is increasingly contested for online tracking'
},
{
keyword: 'intra-group transfers',
baseScore: 6,
description: 'Recital 48 recognizes legitimate interest for internal administrative purposes'
},
{
keyword: 'employee monitoring',
baseScore: 3,
description: 'May be legitimate but heavily dependent on proportionality and transparency'
},
{
keyword: 'legal claims',
baseScore: 7,
description: 'Processing necessary to establish, exercise, or defend legal claims'
},
{
keyword: 'debt collection',
baseScore: 5,
description: 'Recovery of debts owed to the controller'
},
{
keyword: 'service improvement',
baseScore: 2,
description: 'Often too vague - needs specific articulation'
}
];
}
private calculateDataMinimizationScore(necessityTest: NecessityTest): number {
let score = 100;
if (necessityTest.dataVolume === 'extensive') score -= 30;
else if (necessityTest.dataVolume === 'moderate') score -= 15;
if (necessityTest.couldUseAnonymizedData) score -= 25;
if (necessityTest.couldUseLessData) score -= 20;
if (!necessityTest.isRetentionProportionate) score -= 15;
return Math.max(0, score);
}
private generateNecessityRecommendations(
necessityTest: NecessityTest,
issues: string[]
): string[] {
const recommendations: string[] = [];
if (necessityTest.couldUseAnonymizedData) {
recommendations.push('Consider using anonymized or aggregated data instead of personal data.');
}
if (necessityTest.couldUseLessData) {
recommendations.push('Review data collection to identify fields that could be eliminated.');
}
if (!necessityTest.isRetentionProportionate) {
recommendations.push('Implement automatic data deletion after the necessary retention period.');
}
if (necessityTest.alternativesConsidered.length < 2) {
recommendations.push('Document additional alternatives considered and why they were rejected.');
}
return recommendations;
}
private generateBalancingRecommendations(
outcome: string,
issues: string[]
): string[] {
const recommendations: string[] = [];
if (outcome === 'data_subject_prevails') {
recommendations.push('Consider obtaining explicit consent instead of relying on legitimate interest.');
recommendations.push('Review whether the processing is truly necessary for your business.');
}
if (outcome === 'uncertain') {
recommendations.push('Implement additional safeguards to tip the balance in your favor.');
recommendations.push('Consider a Privacy Impact Assessment for further analysis.');
recommendations.push('Consult with your DPO or privacy counsel before proceeding.');
}
if (issues.some(i => i.includes('children'))) {
recommendations.push('Implement robust age verification and parental consent mechanisms.');
}
if (issues.some(i => i.includes('opt-out'))) {
recommendations.push('Implement an easy-to-find and easy-to-use opt-out mechanism.');
}
return recommendations;
}
private generateReasoningSummary(
purposeResult: PurposeTestResult,
necessityResult: NecessityTestResult,
balancingResult: BalancingTestResult
): string {
const parts: string[] = [];
if (!purposeResult.passes) {
parts.push(`Purpose test failed: ${purposeResult.issues.join('; ')}`);
} else {
parts.push('Purpose test passed: Legitimate interest is specific, real, and lawful.');
}
if (!necessityResult.passes) {
parts.push(`Necessity test failed: ${necessityResult.issues.join('; ')}`);
} else {
parts.push(`Necessity test passed with data minimization score of ${necessityResult.dataMinimizationScore}%.`);
}
if (!balancingResult.passes) {
parts.push(`Balancing test result: ${balancingResult.outcome}. ${balancingResult.issues.join('; ')}`);
} else {
parts.push(`Balancing test passed: Controller interests outweigh data subject impact with appropriate safeguards.`);
}
return parts.join('\n\n');
}
private recommendAlternativeBase(assessment: LegitimateInterestAssessment): string {
const { processingActivity } = assessment;
// Suggest alternatives based on processing type
if (processingActivity.toLowerCase().includes('marketing') ||
processingActivity.toLowerCase().includes('advertising')) {
return 'consent';
}
if (processingActivity.toLowerCase().includes('contract') ||
processingActivity.toLowerCase().includes('service delivery')) {
return 'contract';
}
if (processingActivity.toLowerCase().includes('legal') ||
processingActivity.toLowerCase().includes('compliance')) {
return 'legal_obligation';
}
return 'consent';
}
private generateId(): string {
return `lia_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
}
private calculateReviewDate(): Date {
const reviewDate = new Date();
reviewDate.setFullYear(reviewDate.getFullYear() + 1);
return reviewDate;
}
private createEmptyPurposeTest(): PurposeTest {
return {
legitimateInterest: '',
interestHolder: 'controller',
legalBasis: '',
isSpecific: false,
isReal: false,
isCurrent: false,
isLawful: false,
isArticulated: false,
evidence: [],
notes: ''
};
}
private createEmptyNecessityTest(): NecessityTest {
return {
processingDescription: '',
dataCategories: [],
dataVolume: 'minimal',
retentionPeriod: '',
alternativesConsidered: [],
chosenApproach: '',
justification: '',
isProcessingNecessary: false,
couldUseAnonymizedData: false,
couldUseLessData: false,
isRetentionProportionate: false
};
}
private createEmptyBalancingTest(): BalancingTest {
return {
dataSensitivity: 'standard',
dataSubjects: {
categories: [],
includesChildren: false,
includesVulnerable: false,
approximateNumber: 0
},
impactOnDataSubjects: {
physicalHarm: 'none',
financialHarm: 'none',
reputationalHarm: 'none',
emotionalDistress: 'none',
lossOfControl: 'none',
discriminationRisk: 'none',
mitigatingFactors: [],
aggravatingFactors: []
},
relationshipWithDataSubject: '',
wasDataCollectedDirectly: true,
whatWasDataSubjectTold: '',
wouldProcessingBeSurprising: false,
safeguardsInPlace: [],
optOutAvailable: false,
optOutMechanism: '',
controllerInterestStrength: 'moderate',
dataSubjectImpact: 'minimal',
balanceOutcome: 'uncertain'
};
}
}
interface PurposeTestResult {
passes: boolean;
issues: string[];
isRecognizedInterest: boolean;
recommendations: string[];
}
interface NecessityTestResult {
passes: boolean;
issues: string[];
dataMinimizationScore: number;
recommendations: string[];
}
interface BalancingTestResult {
passes: boolean;
outcome: 'controller_prevails' | 'data_subject_prevails' | 'uncertain';
controllerInterestStrength: string;
dataSubjectImpactLevel: string;
issues: string[];
safeguardsRequired: Safeguard[];
recommendations: string[];
}
```
## Common Use Cases: What Works and What Doesn't
### Legitimate Interest: Valid Use Cases
```typescript
interface LegitimateInterestExample {
useCase: string;
legitimateInterest: string;
keyFactors: string[];
requiredSafeguards: string[];
riskLevel: 'low' | 'medium' | 'high';
verdict: 'typically_valid' | 'conditional' | 'typically_invalid';
}
const validUseCases: LegitimateInterestExample[] = [
{
useCase: 'Fraud Prevention',
legitimateInterest: 'Preventing fraudulent transactions and protecting customers',
keyFactors: [
'Explicitly mentioned in GDPR Recital 47',
'Benefits data subjects (customer protection)',
'No less invasive alternative for real-time detection',
'Processing is proportionate to risk'
],
requiredSafeguards: [
'Clear privacy notice about fraud monitoring',
'Limited data retention',
'Access controls on fraud data',
'Regular review of detection rules'
],
riskLevel: 'low',
verdict: 'typically_valid'
},
{
useCase: 'Network and Information Security',
legitimateInterest: 'Maintaining security of IT systems and networks',
keyFactors: [
'Explicitly recognized in Recital 49',
'Essential for protecting all stakeholders',
'Technical logs are necessary for security',
'Limited to security purposes'
],
requiredSafeguards: [
'Purpose limitation to security only',
'Appropriate log retention periods',
'Access restricted to security team',
'No secondary use of security data'
],
riskLevel: 'low',
verdict: 'typically_valid'
},
{
useCase: 'Intra-Group Administrative Transfers',
legitimateInterest: 'Internal administrative purposes within corporate group',
keyFactors: [
'Recognized in Recital 48',
'Includes client/employee data for management',
'Subject to adequate safeguards',
'Not for marketing or profiling purposes'
],
requiredSafeguards: [
'Intra-group data sharing agreement',
'Consistent security standards',
'Clear purpose limitation',
'Appropriate access controls'
],
riskLevel: 'medium',
verdict: 'typically_valid'
},
{
useCase: 'First-Party Website Analytics (Aggregated)',
legitimateInterest: 'Understanding website usage patterns for improvement',
keyFactors: [
'Data is aggregated, not individual tracking',
'No cross-site tracking',
'Clear relationship with data subject',
'Minimal impact on users'
],
requiredSafeguards: [
'Aggregation before storage',
'No individual profiling',
'Clear disclosure in privacy policy',
'Easy opt-out mechanism'
],
riskLevel: 'medium',
verdict: 'conditional'
}
];
const invalidUseCases: LegitimateInterestExample[] = [
{
useCase: 'Personalized Advertising (Third-Party)',
legitimateInterest: 'Serving targeted advertisements based on browsing behavior',
keyFactors: [
'IAB TCF ruling found legitimate interest invalid',
'Users do not expect cross-site tracking',
'Significant impact on privacy',
'Consent is the appropriate basis'
],
requiredSafeguards: [], // Not applicable - consent required
riskLevel: 'high',
verdict: 'typically_invalid'
},
{
useCase: 'Behavioral Profiling Across Services',
legitimateInterest: 'Creating comprehensive user profiles from multiple data sources',
keyFactors: [
'Meta v Bundeskartellamt ruling',
'Users cannot reasonably expect this',
'Significant impact on autonomy',
'Power imbalance with large platforms'
],
requiredSafeguards: [],
riskLevel: 'high',
verdict: 'typically_invalid'
},
{
useCase: 'Selling Data to Third Parties',
legitimateInterest: 'Monetizing user data through third-party sales',
keyFactors: [
'Users do not expect data sales',
'No direct benefit to data subject',
'Severe loss of control',
'Consent clearly required'
],
requiredSafeguards: [],
riskLevel: 'high',
verdict: 'typically_invalid'
},
{
useCase: 'Facial Recognition for Convenience',
legitimateInterest: 'Identifying users through facial recognition for faster service',
keyFactors: [
'Biometric data is special category',
'Article 9 prohibition applies',
'Explicit consent required',
'Cannot use legitimate interest for special categories'
],
requiredSafeguards: [],
riskLevel: 'high',
verdict: 'typically_invalid'
}
];
```
## Integrating Legitimate Interest with Your CMP
Your Consent Management Platform should handle legitimate interest disclosures alongside consent collection:
```typescript
interface CMPLegitimateInterestConfig {
purposes: LegitimateInterestPurpose[];
vendors: LegitimateInterestVendor[];
disclosureStrategy: 'layered' | 'full_upfront' | 'just_in_time';
objectionMechanism: 'toggle' | 'form' | 'email';
}
interface LegitimateInterestPurpose {
id: string;
name: string;
description: string;
legalBasis: 'legitimate_interest';
assessmentId: string; // Link to LIA
dataCategories: string[];
retentionPeriod: string;
objectionEnabled: boolean;
}
interface LegitimateInterestVendor {
id: string;
name: string;
purposeIds: string[];
privacyPolicyUrl: string;
legitimateInterestClaimUrl?: string;
}
class CMPLegitimateInterestManager {
private config: CMPLegitimateInterestConfig;
private objections: Map> = new Map(); // userId -> Set
constructor(config: CMPLegitimateInterestConfig) {
this.config = config;
}
getLegitimateInterestDisclosure(): LegitimateInterestDisclosure {
return {
purposes: this.config.purposes.map(p => ({
id: p.id,
name: p.name,
description: p.description,
canObject: p.objectionEnabled,
dataCategories: p.dataCategories,
retentionPeriod: p.retentionPeriod
})),
vendors: this.config.vendors.map(v => ({
id: v.id,
name: v.name,
purposes: v.purposeIds,
privacyPolicy: v.privacyPolicyUrl
})),
objectionMechanism: this.config.objectionMechanism,
lastUpdated: new Date()
};
}
recordObjection(userId: string, purposeId: string): void {
const userObjections = this.objections.get(userId) || new Set();
userObjections.add(purposeId);
this.objections.set(userId, userObjections);
// Log for compliance
this.logObjection(userId, purposeId);
// Emit event for downstream processing
this.emitObjectionEvent(userId, purposeId);
}
removeObjection(userId: string, purposeId: string): void {
const userObjections = this.objections.get(userId);
if (userObjections) {
userObjections.delete(purposeId);
}
}
hasObjected(userId: string, purposeId: string): boolean {
const userObjections = this.objections.get(userId);
return userObjections?.has(purposeId) || false;
}
canProcessUnderLegitimateInterest(
userId: string,
purposeId: string
): LegitimateInterestDecision {
const purpose = this.config.purposes.find(p => p.id === purposeId);
if (!purpose) {
return {
canProcess: false,
reason: 'Purpose not configured for legitimate interest',
requiresConsent: true
};
}
if (this.hasObjected(userId, purposeId)) {
return {
canProcess: false,
reason: 'Data subject has exercised right to object',
requiresConsent: true,
objectionDate: this.getObjectionDate(userId, purposeId)
};
}
return {
canProcess: true,
reason: 'Legitimate interest applies and no objection recorded',
requiresConsent: false,
assessmentId: purpose.assessmentId
};
}
generateLegitimateInterestBanner(): BannerContent {
const disclosure = this.getLegitimateInterestDisclosure();
return {
title: 'How We Use Your Data',
introduction: `We process some of your data based on our legitimate interests.
This means we have a lawful reason to use your data that doesn't override your rights.
You can object to any of these uses at any time.`,
purposes: disclosure.purposes.map(p => ({
name: p.name,
description: p.description,
objectionButton: p.canObject ? {
text: 'Object',
action: `object_${p.id}`
} : null
})),
learnMoreLink: '/privacy-policy#legitimate-interest',
objectionSuccessMessage: 'Your objection has been recorded. We will stop this processing for you.'
};
}
private logObjection(userId: string, purposeId: string): void {
console.log(`[LI Objection] User ${userId} objected to purpose ${purposeId} at ${new Date().toISOString()}`);
// In production: Write to audit log
}
private emitObjectionEvent(userId: string, purposeId: string): void {
// Emit event for downstream systems to stop processing
const event = new CustomEvent('legitimate-interest:objection', {
detail: { userId, purposeId, timestamp: new Date() }
});
document.dispatchEvent(event);
}
private getObjectionDate(userId: string, purposeId: string): Date | undefined {
// In production: Retrieve from database
return new Date();
}
}
interface LegitimateInterestDisclosure {
purposes: Array<{
id: string;
name: string;
description: string;
canObject: boolean;
dataCategories: string[];
retentionPeriod: string;
}>;
vendors: Array<{
id: string;
name: string;
purposes: string[];
privacyPolicy: string;
}>;
objectionMechanism: string;
lastUpdated: Date;
}
interface LegitimateInterestDecision {
canProcess: boolean;
reason: string;
requiresConsent: boolean;
assessmentId?: string;
objectionDate?: Date;
}
interface BannerContent {
title: string;
introduction: string;
purposes: Array<{
name: string;
description: string;
objectionButton: { text: string; action: string } | null;
}>;
learnMoreLink: string;
objectionSuccessMessage: string;
}
```
## TCF 2.2 and Legitimate Interest
The IAB Transparency and Consent Framework 2.2 includes legitimate interest as a legal basis, but its validity has been challenged:
```typescript
interface TCFLegitimateInterestHandling {
purposeId: number;
purposeName: string;
legitimateInterestAllowed: boolean;
regulatoryGuidance: string;
recommendation: string;
}
const tcf22LegitimateInterestGuidance: TCFLegitimateInterestHandling[] = [
{
purposeId: 1,
purposeName: 'Store and/or access information on a device',
legitimateInterestAllowed: false,
regulatoryGuidance: 'ePrivacy Directive requires consent for device storage/access',
recommendation: 'Always use consent for this purpose'
},
{
purposeId: 2,
purposeName: 'Select basic ads',
legitimateInterestAllowed: false, // Changed in TCF 2.2
regulatoryGuidance: 'Belgian DPA ruling and subsequent TCF updates removed LI',
recommendation: 'Use consent only'
},
{
purposeId: 3,
purposeName: 'Create a personalised ads profile',
legitimateInterestAllowed: false,
regulatoryGuidance: 'Profiling for advertising requires consent',
recommendation: 'Use consent only'
},
{
purposeId: 4,
purposeName: 'Select personalised ads',
legitimateInterestAllowed: false,
regulatoryGuidance: 'Personalized advertising requires consent',
recommendation: 'Use consent only'
},
{
purposeId: 7,
purposeName: 'Measure ad performance',
legitimateInterestAllowed: true, // Conditional
regulatoryGuidance: 'May be valid if truly limited to aggregated measurement',
recommendation: 'Consider consent for safety; if using LI, ensure aggregation'
},
{
purposeId: 8,
purposeName: 'Measure content performance',
legitimateInterestAllowed: true, // Conditional
regulatoryGuidance: 'Similar to ad measurement - aggregation key',
recommendation: 'Prefer consent; LI possible with strong safeguards'
},
{
purposeId: 10,
purposeName: 'Develop and improve products',
legitimateInterestAllowed: true, // Conditional
regulatoryGuidance: 'May be valid for first-party improvement with limitations',
recommendation: 'Acceptable for first-party with clear limitations'
}
];
class TCFLegitimateInterestValidator {
validateVendorClaims(
vendorId: number,
claimedLIPurposes: number[]
): TCFValidationResult {
const invalidClaims: number[] = [];
const conditionalClaims: number[] = [];
const validClaims: number[] = [];
for (const purposeId of claimedLIPurposes) {
const guidance = tcf22LegitimateInterestGuidance.find(
g => g.purposeId === purposeId
);
if (!guidance) {
invalidClaims.push(purposeId);
continue;
}
if (!guidance.legitimateInterestAllowed) {
invalidClaims.push(purposeId);
} else if (guidance.recommendation.includes('conditional') ||
guidance.recommendation.includes('Prefer consent')) {
conditionalClaims.push(purposeId);
} else {
validClaims.push(purposeId);
}
}
return {
vendorId,
isCompliant: invalidClaims.length === 0,
invalidClaims: invalidClaims.map(id => ({
purposeId: id,
reason: `Purpose ${id} cannot use legitimate interest per regulatory guidance`
})),
conditionalClaims: conditionalClaims.map(id => ({
purposeId: id,
reason: `Purpose ${id} legitimate interest is conditional - verify safeguards`
})),
validClaims,
recommendation: this.generateRecommendation(invalidClaims, conditionalClaims)
};
}
private generateRecommendation(
invalid: number[],
conditional: number[]
): string {
if (invalid.length > 0) {
return `Remove legitimate interest claims for purposes ${invalid.join(', ')}. Use consent instead.`;
}
if (conditional.length > 0) {
return `Review legitimate interest claims for purposes ${conditional.join(', ')}. Ensure appropriate safeguards and consider switching to consent.`;
}
return 'Legitimate interest claims appear compliant with current guidance.';
}
}
interface TCFValidationResult {
vendorId: number;
isCompliant: boolean;
invalidClaims: Array<{ purposeId: number; reason: string }>;
conditionalClaims: Array<{ purposeId: number; reason: string }>;
validClaims: number[];
recommendation: string;
}
```
## Automating LIA Reviews
Legitimate Interest Assessments should be reviewed regularly. Here's an automated review system:
```typescript
interface LIAReviewSchedule {
assessmentId: string;
lastReviewDate: Date;
nextReviewDate: Date;
reviewTriggers: ReviewTrigger[];
reviewStatus: 'current' | 'review_due' | 'overdue';
}
interface ReviewTrigger {
type: 'time_based' | 'regulatory_change' | 'business_change' | 'complaint';
description: string;
triggeredAt?: Date;
}
class LIAReviewManager {
private schedules: Map = new Map();
private assessmentEngine: LegitimateInterestAssessmentEngine;
constructor(assessmentEngine: LegitimateInterestAssessmentEngine) {
this.assessmentEngine = assessmentEngine;
}
scheduleReview(
assessmentId: string,
reviewFrequencyMonths: number = 12
): LIAReviewSchedule {
const now = new Date();
const nextReview = new Date(now);
nextReview.setMonth(nextReview.getMonth() + reviewFrequencyMonths);
const schedule: LIAReviewSchedule = {
assessmentId,
lastReviewDate: now,
nextReviewDate: nextReview,
reviewTriggers: [
{
type: 'time_based',
description: `Scheduled ${reviewFrequencyMonths}-month review`
}
],
reviewStatus: 'current'
};
this.schedules.set(assessmentId, schedule);
return schedule;
}
triggerReview(
assessmentId: string,
trigger: ReviewTrigger
): void {
const schedule = this.schedules.get(assessmentId);
if (!schedule) return;
trigger.triggeredAt = new Date();
schedule.reviewTriggers.push(trigger);
schedule.reviewStatus = 'review_due';
this.schedules.set(assessmentId, schedule);
// Notify relevant stakeholders
this.notifyReviewRequired(assessmentId, trigger);
}
checkAllSchedules(): LIAReviewReport {
const now = new Date();
const overdueReviews: string[] = [];
const upcomingReviews: string[] = [];
const triggeredReviews: string[] = [];
for (const [assessmentId, schedule] of this.schedules.entries()) {
// Check if overdue
if (schedule.nextReviewDate < now && schedule.reviewStatus !== 'overdue') {
schedule.reviewStatus = 'overdue';
overdueReviews.push(assessmentId);
}
// Check if due within 30 days
const thirtyDaysFromNow = new Date(now);
thirtyDaysFromNow.setDate(thirtyDaysFromNow.getDate() + 30);
if (schedule.nextReviewDate <= thirtyDaysFromNow &&
schedule.nextReviewDate > now &&
schedule.reviewStatus === 'current') {
upcomingReviews.push(assessmentId);
}
// Check for triggered reviews
if (schedule.reviewStatus === 'review_due') {
triggeredReviews.push(assessmentId);
}
}
return {
generatedAt: now,
totalAssessments: this.schedules.size,
overdueReviews,
upcomingReviews,
triggeredReviews,
summary: this.generateSummary(overdueReviews, upcomingReviews, triggeredReviews)
};
}
monitorRegulatoryChanges(): void {
// In production: Subscribe to regulatory update feeds
// Example triggers that would require review:
const potentialTriggers = [
'New CJEU ruling on legitimate interest',
'Updated EDPB guidelines',
'National DPA enforcement decision',
'Changes to ePrivacy Directive interpretation'
];
// Simulate checking for updates
console.log('Monitoring regulatory changes:', potentialTriggers);
}
recordComplaint(
assessmentId: string,
complaintDetails: string
): void {
this.triggerReview(assessmentId, {
type: 'complaint',
description: `Data subject complaint: ${complaintDetails}`
});
}
recordBusinessChange(
assessmentId: string,
changeDetails: string
): void {
this.triggerReview(assessmentId, {
type: 'business_change',
description: `Business change affecting processing: ${changeDetails}`
});
}
completeReview(
assessmentId: string,
reviewOutcome: 'confirmed' | 'updated' | 'withdrawn'
): void {
const schedule = this.schedules.get(assessmentId);
if (!schedule) return;
const now = new Date();
schedule.lastReviewDate = now;
schedule.nextReviewDate = new Date(now);
schedule.nextReviewDate.setFullYear(schedule.nextReviewDate.getFullYear() + 1);
schedule.reviewStatus = 'current';
// Clear non-time-based triggers after review
schedule.reviewTriggers = schedule.reviewTriggers.filter(
t => t.type === 'time_based'
);
this.schedules.set(assessmentId, schedule);
// Log review completion for audit trail
this.logReviewCompletion(assessmentId, reviewOutcome);
}
private notifyReviewRequired(
assessmentId: string,
trigger: ReviewTrigger
): void {
console.log(`[LIA Review] Assessment ${assessmentId} requires review: ${trigger.description}`);
// In production: Send email/notification to DPO and relevant stakeholders
}
private generateSummary(
overdue: string[],
upcoming: string[],
triggered: string[]
): string {
const parts: string[] = [];
if (overdue.length > 0) {
parts.push(`⚠️ ${overdue.length} assessment(s) overdue for review`);
}
if (triggered.length > 0) {
parts.push(`🔔 ${triggered.length} assessment(s) triggered for review`);
}
if (upcoming.length > 0) {
parts.push(`📅 ${upcoming.length} assessment(s) due for review within 30 days`);
}
if (parts.length === 0) {
return '✅ All assessments are current';
}
return parts.join('\n');
}
private logReviewCompletion(
assessmentId: string,
outcome: string
): void {
console.log(`[LIA Review Complete] Assessment ${assessmentId}: ${outcome} at ${new Date().toISOString()}`);
// In production: Write to audit log
}
}
interface LIAReviewReport {
generatedAt: Date;
totalAssessments: number;
overdueReviews: string[];
upcomingReviews: string[];
triggeredReviews: string[];
summary: string;
}
```
## FAQ
### Can I switch from consent to legitimate interest if users decline?
No. If you initially asked for consent and the user declined, you cannot then rely on legitimate interest for the same processing. This would undermine the user's choice and is not compliant with GDPR. You must respect the user's decision.
### Does legitimate interest still apply to direct marketing?
GDPR Recital 47 mentions that direct marketing "may be regarded as carried out for a legitimate interest." However, this has been significantly narrowed by enforcement. Email marketing to existing customers (soft opt-in) may still qualify in some jurisdictions, but behavioral advertising and online tracking for marketing purposes typically requires consent.
### How often should I review my Legitimate Interest Assessments?
At minimum, review LIAs annually. You should also trigger a review when:
- Regulatory guidance changes
- There's a relevant court decision
- You receive complaints about the processing
- The business context changes materially
- The volume or nature of data changes
### What's the difference between "legitimate interest" and "vital interest"?
Legitimate interest (Article 6(1)(f)) is for situations where the controller's business interests justify processing. Vital interest (Article 6(1)(d)) is only for life-threatening situations where processing is necessary to protect someone's life. They serve completely different purposes.
### Can I use legitimate interest for processing children's data?
GDPR Article 6(1)(f) specifically mentions that children's interests carry extra weight in the balancing test. While not absolutely prohibited, using legitimate interest for children's data is very difficult to justify. Consent (with parental consent for children under the digital age of consent) is almost always the appropriate basis.
## Applying the Framework: A Practical Checklist
Before relying on legitimate interest, work through this checklist:
**Step 1: Purpose Test**
- [ ] Is my interest specific and clearly articulated?
- [ ] Is it a real, current interest (not speculative)?
- [ ] Is it lawful and ethical?
- [ ] Can I document evidence supporting this interest?
**Step 2: Necessity Test**
- [ ] Is this processing truly necessary for my purpose?
- [ ] Have I considered less invasive alternatives?
- [ ] Could I achieve the same with anonymized/aggregated data?
- [ ] Am I collecting only the minimum necessary data?
- [ ] Is my retention period proportionate?
**Step 3: Balancing Test**
- [ ] What is the impact on data subjects?
- [ ] Would they expect this processing?
- [ ] Does it include children or vulnerable individuals?
- [ ] Have I implemented appropriate safeguards?
- [ ] Is there an easy opt-out mechanism?
- [ ] Does my interest clearly outweigh theirs?
**Documentation**
- [ ] Is my LIA documented and dated?
- [ ] Is it available for regulatory inspection?
- [ ] Is it referenced in my privacy policy?
- [ ] Is there a review schedule in place?
If you can't confidently check all these boxes, consent is likely the more appropriate legal basis. When in doubt, consent provides the strongest legal protection for both you and your users.