Skip to main content

Regulatory Compliance Overview

The Baanx Consent Management API is designed to help you meet data privacy regulations including GDPR, CCPA, and other regional requirements. This guide covers compliance considerations and best practices.
This guide provides technical implementation guidance. Consult with legal counsel to ensure your specific implementation meets all applicable regulatory requirements.

GDPR Compliance

The General Data Protection Regulation (GDPR) applies to organizations processing personal data of EU residents.

Key GDPR Requirements

Lawful Basis

Establish legal basis for data processing (consent, contract, legitimate interest)

Explicit Consent

Consent must be freely given, specific, informed, and unambiguous

Right to Withdraw

Users can withdraw consent as easily as they gave it

Records of Consent

Maintain proof of when and how consent was obtained
1

Clear Communication

Inform users before data collection about:
  • What data you’re collecting
  • Why you’re collecting it
  • How long you’ll retain it
  • Who you’ll share it with
  • Their rights (access, rectification, erasure, portability)
2

Granular Consent

Separate consent for different purposes:
const consents = [
  { type: 'termsAndPrivacy', status: 'granted' },      // Required
  { type: 'marketingNotifications', status: 'denied' }, // Optional
  { type: 'smsNotifications', status: 'denied' },       // Optional
  { type: 'emailNotifications', status: 'granted' }     // Optional
];
Users must be able to opt-in to marketing separately from service requirements.
3

Affirmative Action

Require explicit opt-in (no pre-checked boxes):
// ✅ Good: User must actively check
<Checkbox checked={marketingConsent} onChange={setMarketingConsent}>
  I agree to receive marketing communications
</Checkbox>

// ❌ Bad: Pre-checked by default
<Checkbox checked={true} onChange={setMarketingConsent}>
  I agree to receive marketing communications
</Checkbox>
4

Record Keeping

Capture comprehensive metadata:
metadata: {
  ipAddress: req.ip,
  userAgent: req.headers['user-agent'],
  timestamp: new Date().toISOString(),
  consentMethod: 'registration_form',
  privacyPolicyVersion: 'v2.1',
  termsVersion: 'v3.0'
}
5

Easy Withdrawal

Implement straightforward revocation:
async function revokeMarketingConsent(userId: string) {
  const { consentSets } = await consentManager.getUserConsentStatus(
    userId,
    true
  );

  const marketingConsent = consentSets[0].consents.find(
    c => c.consentType === 'marketingNotifications' && c.consentStatus === 'granted'
  );

  if (marketingConsent) {
    await consentManager.revokeConsent(
      consentSets[0].consentSetId,
      marketingConsent.consentId
    );
  }
}

Right to Be Forgotten (GDPR Article 17)

When users request data deletion:
  1. Revoke all consents:
    const { consentSets } = await consentManager.getUserConsentStatus(userId, true);
    
    for (const consentSet of consentSets) {
      for (const consent of consentSet.consents) {
        if (consent.consentStatus === 'granted') {
          await consentManager.revokeConsent(
            consentSet.consentSetId,
            consent.consentId
          );
        }
      }
    }
    
  2. Export audit trail (for your records):
    const auditTrail = await consentManager.getAuditTrail(userId, 1000, 0);
    await saveAuditForLegalHold(userId, auditTrail);
    
  3. Process deletion in your systems (user data, accounts, etc.)
The Baanx API maintains consent records even after revocation for regulatory compliance. This is permissible under GDPR as legitimate interest for legal defense.

CCPA Compliance

The California Consumer Privacy Act (CCPA) applies to businesses collecting personal information of California residents.

Key CCPA Requirements

RequirementImplementation
Notice at CollectionInform users at/before data collection what categories of data you’re collecting and why
Opt-Out of SaleProvide “Do Not Sell My Personal Information” option
Access & DeletionEnable users to request their data and deletion
Non-DiscriminationDon’t penalize users who exercise privacy rights

CCPA-Compliant Implementation

Notice at Collection

Display clear privacy notice:
<ConsentForm>
  <PrivacyNotice>
    We collect personal information including name, email, phone number, and
    location data to provide financial services. View our full privacy policy
    for details on data categories, purposes, and third-party sharing.
  </PrivacyNotice>

  <ConsentCheckbox type="termsAndPrivacy" required />
  <ConsentCheckbox type="emailNotifications" optional />
</ConsentForm>

Opt-Out Mechanisms

Implement opt-out for data sales/sharing:
const consents = [
  { type: 'termsAndPrivacy', status: 'granted' },
  { type: 'marketingNotifications', status: 'denied' }, // Opt-out of marketing
  { type: 'emailNotifications', status: 'denied' }      // Opt-out of emails
];
The denied status in consent records documents the user’s opt-out decision for regulatory compliance.

Audit Trail Management

The API maintains an immutable audit trail for all consent changes. Use this for regulatory compliance and investigations.

What’s Recorded in Audit Trail

Each audit record contains:
{
  "auditId": "audit_001",
  "action": "created | updated | revoked",
  "timestamp": "ISO 8601 date-time",
  "consentSetId": "UUID of consent set",
  "changes": {
    "before": {
      "consentType": "marketingNotifications",
      "consentStatus": "granted"
    },
    "after": {
      "consentType": "marketingNotifications",
      "consentStatus": "revoked"
    }
  },
  "metadata": {
    "ipAddress": "192.168.1.1",
    "userAgent": "Mozilla/5.0...",
    "timestamp": "2024-01-15T10:30:00Z"
  }
}

Audit Trail Use Cases

Export complete consent history for regulatory examinations:
async function exportConsentAudit(userId: string) {
  let allRecords = [];
  let offset = 0;
  const limit = 100;

  while (true) {
    const { auditRecords, pagination } = await consentManager.getAuditTrail(
      userId,
      limit,
      offset
    );

    allRecords.push(...auditRecords);

    if (offset + limit >= pagination.total) break;
    offset += limit;
  }

  return generateAuditReport(userId, allRecords);
}
Users can request their consent history:
async function handleDSAR(userId: string) {
  const { consentStatus, consentSets } = await consentManager.getUserConsentStatus(
    userId,
    true
  );

  const auditTrail = await consentManager.getAuditTrail(userId, 1000, 0);

  return {
    currentStatus: consentStatus,
    consentSets: consentSets,
    changeHistory: auditTrail.auditRecords
  };
}
Generate periodic compliance reports:
async function generateComplianceReport(startDate: string, endDate: string) {
  // Query all users who completed registration in date range
  const users = await getUsersRegisteredBetween(startDate, endDate);

  const report = {
    totalUsers: users.length,
    completeConsents: 0,
    incompleteConsents: 0,
    revocations: 0
  };

  for (const user of users) {
    const { consentStatus } = await consentManager.getUserConsentStatus(user.id);

    if (consentStatus === 'complete') report.completeConsents++;
    else report.incompleteConsents++;

    const { auditRecords } = await consentManager.getAuditTrail(user.id);
    report.revocations += auditRecords.filter(
      r => r.action === 'revoked'
    ).length;
  }

  return report;
}

Best Practices

1. Version Your Policies

Track policy versions in metadata to prove which version users consented to:
metadata: {
  privacyPolicyVersion: 'v2.1',
  termsOfServiceVersion: 'v3.0',
  policyUrl: 'https://example.com/privacy-v2.1',
  timestamp: new Date().toISOString()
}
When policies change significantly, require re-consent:
async function checkPolicyVersions(userId: string) {
  const { consentSets } = await consentManager.getUserConsentStatus(userId, true);
  const latestConsent = consentSets[0];

  const consentedVersion = latestConsent.consents[0].metadata?.privacyPolicyVersion;
  const currentVersion = 'v3.0';

  if (consentedVersion !== currentVersion) {
    return { requiresReconsent: true, reason: 'policy_update' };
  }

  return { requiresReconsent: false };
}
Consider re-confirming consent periodically:
async function checkConsentAge(userId: string, maxAgeMonths: number = 24) {
  const { consentSets } = await consentManager.getUserConsentStatus(userId, true);
  const latestConsent = consentSets[0];

  const consentDate = new Date(latestConsent.createdAt);
  const now = new Date();
  const monthsSinceConsent = (now.getTime() - consentDate.getTime()) / (1000 * 60 * 60 * 24 * 30);

  if (monthsSinceConsent > maxAgeMonths) {
    return { requiresReconsent: true, reason: 'consent_expired' };
  }

  return { requiresReconsent: false };
}

3. Separate Required vs Optional Consents

Make it clear which consents are necessary for service:
const consentConfig = {
  required: ['termsAndPrivacy', 'eSignAct'],
  optional: ['marketingNotifications', 'smsNotifications', 'emailNotifications']
};

function validateConsents(consents: ConsentItem[]) {
  const requiredGranted = consentConfig.required.every(type =>
    consents.some(c => c.consentType === type && c.consentStatus === 'granted')
  );

  if (!requiredGranted) {
    throw new Error('Required consents must be granted');
  }

  return true;
}
UI Example:
<ConsentForm>
  <RequiredSection>
    <h3>Required for Service</h3>
    <Checkbox required>Terms of Service & Privacy Policy</Checkbox>
    <Checkbox required>Electronic Signature Agreement</Checkbox>
  </RequiredSection>

  <OptionalSection>
    <h3>Optional</h3>
    <Checkbox>Marketing Communications (you can opt out anytime)</Checkbox>
    <Checkbox>SMS Notifications</Checkbox>
    <Checkbox>Email Notifications</Checkbox>
  </OptionalSection>
</ConsentForm>
Build user-facing consent preferences:
function ConsentPreferences({ userId }: { userId: string }) {
  const [consents, setConsents] = useState<ConsentSetDetail[]>([]);

  useEffect(() => {
    const fetchConsents = async () => {
      const { consentSets } = await consentManager.getUserConsentStatus(
        userId,
        true
      );
      setConsents(consentSets);
    };

    fetchConsents();
  }, [userId]);

  const handleToggle = async (consentSetId: string, consentId: string, currentStatus: string) => {
    if (currentStatus === 'granted') {
      await consentManager.revokeConsent(consentSetId, consentId);
    } else {
      // Create new consent record with 'granted' status
      // (requires new endpoint or re-consent flow)
    }

    // Refresh consents
  };

  return (
    <Card>
      <h2>Privacy Preferences</h2>
      {consents[0]?.consents.map(consent => (
        <ConsentToggle
          key={consent.consentId}
          consent={consent}
          onToggle={() => handleToggle(
            consents[0].consentSetId,
            consent.consentId,
            consent.consentStatus
          )}
        />
      ))}
    </Card>
  );
}
Maintain application-level logs for consent operations:
async function createConsentWithLogging(
  onboardingId: string,
  consents: ConsentItem[],
  metadata: any
) {
  logger.info('Creating consent set', {
    onboardingId,
    consentTypes: consents.map(c => c.consentType),
    timestamp: new Date().toISOString()
  });

  try {
    const result = await consentManager.createOnboardingConsent(
      onboardingId,
      consents,
      metadata
    );

    logger.info('Consent set created successfully', {
      consentSetId: result.consentSetId,
      onboardingId
    });

    return result;
  } catch (error) {
    logger.error('Consent creation failed', {
      onboardingId,
      error: error.message
    });

    throw error;
  }
}
Guide users to complete required consents:
async function checkAndEnforceConsent(userId: string) {
  const { consentStatus, consentSets } = await consentManager.getUserConsentStatus(
    userId,
    true
  );

  if (consentStatus === 'incomplete') {
    const missingConsents = identifyMissingConsents(consentSets[0]);

    return {
      allowed: false,
      reason: 'incomplete_consent',
      action: 'redirect_to_consent_page',
      missingConsents
    };
  }

  return { allowed: true };
}

function identifyMissingConsents(consentSet: ConsentSetDetail) {
  const requiredTypes = consentSet.policyType === 'US'
    ? ['eSignAct', 'termsAndPrivacy', 'marketingNotifications', 'smsNotifications', 'emailNotifications']
    : ['termsAndPrivacy', 'marketingNotifications', 'smsNotifications', 'emailNotifications'];

  const grantedTypes = consentSet.consents
    .filter(c => c.consentStatus === 'granted')
    .map(c => c.consentType);

  return requiredTypes.filter(type => !grantedTypes.includes(type));
}

Data Retention

  • Active consent records: Retain indefinitely while user account is active
  • Revoked consents: Retain for legal defense (typically 7 years minimum)
  • Audit trail: Retain according to regulatory requirements (GDPR: 6 years, CCPA: varies)

Deletion After Account Closure

When a user closes their account:
  1. Retain consent audit trail for legal hold period
  2. Mark records as “account closed” but don’t delete
  3. After legal hold expires, anonymize consent records (remove PII from metadata)
async function handleAccountClosure(userId: string) {
  const auditTrail = await consentManager.getAuditTrail(userId, 1000, 0);

  await archiveForLegalHold(userId, auditTrail, {
    retentionYears: 7,
    reason: 'account_closure'
  });

  scheduleAnonymization(userId, addYears(new Date(), 7));
}

Regional Considerations

US-Specific Requirements

  • Use policyType: "US" for US-based users
  • Requires eSignAct consent for E-Sign Act compliance
  • Consider state-specific laws (CCPA in California, VCDPA in Virginia, etc.)
  • All 5 consent types required

EU-Specific Requirements

  • Use policyType: "global" for EU users
  • 4 consent types (excludes eSignAct)
  • Implement “Right to Object” for automated processing
  • Provide data portability mechanisms

Multi-Region Deployments

Use the x-us-env header to route requests correctly:
const headers = {
  'x-client-key': clientKey,
  'x-secret-key': secretKey
};

if (userRegion === 'US') {
  headers['x-us-env'] = 'true';
}

await fetch('https://api.baanx.com/v2/consent/onboarding', {
  method: 'POST',
  headers,
  body: JSON.stringify(requestBody)
});

Next Steps