From hubspot-pack
Implements HubSpot GDPR/CCPA contact deletion, data export for DSAR, and privacy ops using CRM APIs. For compliance in integrations.
npx claudepluginhub jeremylongshore/claude-code-plugins-plus-skills --plugin hubspot-packThis skill is limited to using the following tools:
Handle GDPR/CCPA compliance with HubSpot's built-in privacy APIs: GDPR delete, data export, consent management, and PII handling for CRM data.
Interact with HubSpot CRM and CMS REST APIs via curl to create, list, search, and manage contacts, companies, deals, owners, and content.
Guides GDPR/CCPA-compliant Intercom data handling: DSAR exports, contact deletion, PII redaction, retention policies using TypeScript SDK.
Provides expert patterns for HubSpot CRM integration including OAuth authentication, CRM objects, associations, batch operations, webhooks, and custom objects using Node.js and Python SDKs.
Share bugs, ideas, or general feedback.
Handle GDPR/CCPA compliance with HubSpot's built-in privacy APIs: GDPR delete, data export, consent management, and PII handling for CRM data.
crm.objects.contacts.write (for GDPR delete)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}`);
}
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;
}
// 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" }
// 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',
},
});
}
}
// 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
]);
| 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 |
For enterprise access control, see hubspot-enterprise-rbac.