Tools like CallRail push phone calls straight into HubSpot, creating contacts that often have no email address. Native duplicate management leans heavily on email, so phone-only contacts pile up fast. The custom code action below finds those records, confirms they match on full name and address, and merges them automatically—keeping your CRM tidy without manual intervention.
- Eliminates duplicate contacts generated by call-tracking integrations.
- Merges records in real time, before they trigger workflows or reports.
- Uses city and street address for higher confidence when email is missing.
Typical Use Cases
Scenario | Why It Matters |
---|---|
CallRail or similar phone-tracking apps | Each phone call can create a new phone-only contact; merging prevents duplicates in reporting and sequences. |
Brick-and-mortar businesses | Customers often provide phone and address at checkout but not email; address helps verify true duplicates. |
Service industries with repeat callers | Returning clients may call again from ads, generating extra contacts that clutter pipelines. |
Step-by-Step Implementation
1. Prerequisites
- Create a private app token with
crm.objects.contacts.read
andcrm.objects.contacts.write
scopes. Store it in the workflow action asMyContacts
. - Make sure your earlier deduplication step flags records with
likely_duplicated_by = Full Name and Address
.
2. Insert the Custom Code Action
- Create or edit a Contact-based workflow that runs after the duplicate-flagging step.
- Add a Custom Code action and reference the secret
MyContacts
. - Paste the script below.
const hubspot = require('@hubspot/api-client');
exports.main = async (event) => {
const hubspotContacts = new hubspot.Client({
accessToken: process.env.MyContacts
});
// 1. Get current contact details
let id, first, last, city, street, flag;
try {
const res = await hubspotContacts.crm.contacts.basicApi.getById(
event.object.objectId,
['hs_object_id','firstname','lastname','city','address','likely_duplicated_by']
);
id = res.body.properties.hs_object_id;
first = res.body.properties.firstname;
last = res.body.properties.lastname;
city = res.body.properties.city;
street = res.body.properties.address;
flag = res.body.properties.likely_duplicated_by;
} catch (err) {
console.error(err);
throw err;
}
// 2. Only proceed if this record was flagged by previous step
if (flag !== 'Full Name and Address') return;
// 3. Search for duplicates with same name + address, exclude self
let dupIds = [];
try {
const search = await hubspotContacts.apiRequest({
method: 'POST',
path: '/crm/v3/objects/contacts/search',
body: {
filterGroups: [{
filters: [
{ propertyName:'city', operator:'EQ', value: city },
{ propertyName:'address', operator:'EQ', value: street },
{ propertyName:'likely_duplicated_by', operator:'EQ', value:'Full Name and Address' }
]
}],
limit: 100
}
});
dupIds = search.body.results
.filter(c => Number(c.id) !== Number(id) &&
c.properties.firstname === first &&
c.properties.lastname === last)
.map(c => c.id);
} catch (err) {
console.error(err);
throw err;
}
// 4. Merge if exactly one duplicate is found
if (dupIds.length === 1) {
try {
await hubspotContacts.apiRequest({
method: 'POST',
path: '/contacts/v1/contact/merge-vids/' + dupIds[0],
body: { vidToMerge: id }
});
} catch (err) {
console.error(err);
throw err;
}
}
};
3. Test Thoroughly
- Create two contacts through CallRail with same name and address.
- Run the duplicate-flagging step first, then this merge step.
- Confirm one master record remains and timelines are combined.
- Create a third contact with same city but different address and verify it is not merged.
Results and Benefits
- Phone-only duplicates from call tracking are merged automatically.
- Sales reps see a single timeline per caller, improving context and follow-up.
- Lists, dashboards and attribution remain accurate without manual cleanup.
Wrapping Up
Email-centric deduplication misses many contacts created by call-tracking systems. By pairing a flag-then-merge workflow with custom code, you can keep HubSpot clean even when new records arrive with only a phone number. Adapt the filters to include other identifiers such as state or zip if your data requires tighter matching.