HubSpot Data Handling
Overview
Handle GDPR/CCPA compliance with HubSpot's built-in privacy APIs: GDPR delete, data export, consent management, and PII handling for CRM data.
Prerequisites
- HubSpot account with GDPR features enabled
- Scope:
crm.objects.contacts.write(for GDPR delete) - Understanding of GDPR/CCPA requirements
Instructions
Step 1: GDPR Contact Deletion
HubSpot provides a dedicated GDPR delete endpoint that permanently removes all contact data and communications:
import * as hubspot from '@hubspot/api-client';
const client = new hubspot.Client({
accessToken: process.env.HUBSPOT_ACCESS_TOKEN!,
});
// GDPR delete: permanently removes contact and all associated data
// POST /crm/v3/objects/contacts/gdpr-delete
async function gdprDeleteContact(email: string): Promise<void> {
// First, find the contact
const search = await client.crm.contacts.searchApi.doSearch({
filterGroups: [{
filters: [{ propertyName: 'email', operator: 'EQ', value: email }],
}],
properties: ['email'],
limit: 1, after: 0, sorts: [],
});
if (search.results.length === 0) {
console.log(`Contact not found: ${email}`);
return;
}
const contactId = search.results[0].id;
// GDPR delete via API
await client.apiRequest({
method: 'POST',
path: '/crm/v3/objects/contacts/gdpr-delete',
body: {
objectId: contactId,
idProperty: 'hs_object_id',
},
});
// Also delete from your local systems
await deleteLocalContactData(email);
// Audit log (keep for compliance -- do NOT delete audit records)
await auditLog({
action: 'GDPR_DELETE',
email: '[REDACTED]', // don't store the email in audit
contactId,
timestamp: new Date().toISOString(),
reason: 'Data subject deletion request',
});
console.log(`GDPR deleted contact ${contactId}`);
}
Step 2: Data Subject Access Request (DSAR)
Export all data HubSpot holds about a contact:
async function exportContactData(email: string): Promise<ContactDataExport> {
// Find contact
const search = await client.crm.contacts.searchApi.doSearch({
filterGroups: [{
filters: [{ propertyName: 'email', operator: 'EQ', value: email }],
}],
properties: [], // get all default properties
limit: 1, after: 0, sorts: [],
});
if (search.results.length === 0) {
return { found: false, data: null };
}
const contact = search.results[0];
// Get all properties for complete export
const fullContact = await client.crm.contacts.basicApi.getById(
contact.id,
undefined, // all properties
undefined,
['companies', 'deals', 'tickets'] // include associations
);
// Get associated deals
const deals = await client.crm.deals.searchApi.doSearch({
filterGroups: [{
filters: [{
propertyName: 'associations.contact',
operator: 'EQ',
value: contact.id,
}],
}],
properties: ['dealname', 'amount', 'dealstage', 'createdate'],
limit: 100, after: 0, sorts: [],
});
// Get engagement history (notes, emails, calls)
const notes = await client.crm.objects.notes.basicApi.getPage(
100, undefined, ['hs_note_body', 'hs_timestamp']
);
return {
found: true,
data: {
exportedAt: new Date().toISOString(),
source: 'HubSpot CRM',
contact: fullContact.properties,
associations: fullContact.associations,
deals: deals.results.map(d => d.properties),
notes: notes.results.map(n => n.properties),
},
};
}
interface ContactDataExport {
found: boolean;
data: {
exportedAt: string;
source: string;
contact: Record<string, string>;
associations?: any;
deals: Record<string, string>[];
notes: Record<string, string>[];
} | null;
}
Step 3: PII Redaction for Logging
// Never log PII from HubSpot responses
const PII_FIELDS = new Set([
'email', 'firstname', 'lastname', 'phone', 'mobilephone',
'address', 'city', 'state', 'zip', 'country',
'date_of_birth', 'ip_city', 'ip_state', 'ip_country',
]);
function redactContactForLogging(properties: Record<string, string>): Record<string, string> {
const redacted: Record<string, string> = {};
for (const [key, value] of Object.entries(properties)) {
redacted[key] = PII_FIELDS.has(key) ? '[REDACTED]' : value;
}
return redacted;
}
// Usage
const contact = await client.crm.contacts.basicApi.getById(id, ['email', 'lifecyclestage']);
console.log('Contact data:', redactContactForLogging(contact.properties));
// Output: { email: "[REDACTED]", lifecyclestage: "customer" }
Step 4: Consent Tracking
// Track consent using HubSpot's communication preferences
// POST /crm/v3/objects/contacts (with consent properties)
async function createContactWithConsent(
email: string,
properties: Record<string, string>,
consent: { marketing: boolean; sales: boolean }
): Promise<void> {
await client.crm.contacts.basicApi.create({
properties: {
...properties,
email,
hs_legal_basis: consent.marketing
? 'Legitimate interest - existing customer'
: 'Not applicable',
},
associations: [],
});
// Set communication preferences via the subscriptions API
if (consent.marketing) {
await client.apiRequest({
method: 'POST',
path: `/communication-preferences/v3/subscribe`,
body: {
emailAddress: email,
subscriptionId: process.env.HUBSPOT_MARKETING_SUBSCRIPTION_ID!,
legalBasis: 'CONSENT_WITH_NOTICE',
legalBasisExplanation: 'User opted in via signup form',
},
});
}
}
Step 5: Data Minimization
// Only request the properties you actually need
// BAD: Fetches all default properties including PII
const bad = await client.crm.contacts.basicApi.getById(id);
// GOOD: Only fetch non-PII fields for analytics
const good = await client.crm.contacts.basicApi.getById(id, [
'lifecyclestage', // not PII
'hs_lead_status', // not PII
'createdate', // not PII
'num_associated_deals', // not PII
]);
Output
- GDPR delete endpoint permanently removing contact data
- Data export for Subject Access Requests
- PII redaction utility for safe logging
- Consent tracking with communication preferences
- Data minimization patterns for non-PII analytics
Error Handling
| Issue | Cause | Solution |
|-------|-------|----------|
| GDPR delete returns 404 | Contact already deleted | Idempotent -- log and continue |
| Export missing associations | Scope not granted | Add crm.objects.deals.read scope |
| Consent API returns 400 | Invalid subscription ID | Check Settings > Marketing > Email > Subscriptions |
| PII in logs | Missing redaction | Wrap all logging with redactContactForLogging |
Resources
Next Steps
For enterprise access control, see hubspot-enterprise-rbac.