⚙ Operations Hub

customtruck
Participant

Cannot merge two contacts using custom code in workflow

SOLVE

I am attempting to use the custom code workflow capabilities in Operation Hub to merge two contacts if they match on the mobilephone (internal name) property. This is very similar to the use case provided by Hubspot staff here sample-workflow-custom-code/samples/dedupe_contact.js at main · HubSpot/sample-workflow-custom-code ...

However, that code does nothing anymore when run and gives linting tips that mergeResults is defined but never used, and uses the now deprecated v1 APIs.

I've tried rewriting it to use the v3 merge API and the log does show that the workflow successfully found a matching contact ID with the same mobilephone number as the enrolled contact's, and it says it successfully sent the merge request to the merge API for those two IDs. But they are not merged. Any idea why? Here's the current node.js code:

Spoiler

/**
* HubSpot Workflow Custom Code: Merge contacts by matching mobilephone
*
* Logic:
* - If NO other contact matches → do nothing
* - If EXACTLY 1 other contact matches → MERGE enrolled contact into that contact
* - If 2+ other contacts match → skip merge (ambiguous)
*/

const hubspot = require('@hubspot/api-client');
const DEDUPE_PROPERTY = 'mobilephone';

exports.main = async (event, callback) => {
const hubspotClient = new hubspot.Client({
accessToken: process.env.[SECRET]
});

try {
const enrolledId = event.object.objectId;
const enrolledIdStr = String(enrolledId);

// 1) Fetch enrolled contact's mobilephone
const contact = await hubspotClient.crm.contacts.basicApi.getById(
enrolledId,
[DEDUPE_PROPERTY]
);

const phoneValue = contact.properties?.[DEDUPE_PROPERTY];

if (!phoneValue) {
console.log('No mobilephone on enrolled contact; nothing to merge.');
return callback(null, { status: 'no_mobilephone' });
}

console.log(`Searching for contacts with ${DEDUPE_PROPERTY} = ${phoneValue}`);

// 2) Search for contacts with matching mobilephone
const search = await hubspotClient.crm.contacts.searchApi.doSearch({
filterGroups: [{
filters: [{
propertyName: DEDUPE_PROPERTY,
operator: 'EQ',
value: phoneValue
}]
}],
limit: 10
});

const matchingIds = (search.results || [])
.map(c => c.id)
.filter(id => String(id) !== enrolledIdStr);

console.log(`Found ${matchingIds.length} other contacts with same mobilephone.`);

// --- Merge Logic ---
if (matchingIds.length === 0) {
console.log('No duplicates found. Nothing to merge.');
return callback(null, { status: 'no_duplicates' });
}

if (matchingIds.length >= 2) {
console.log('Ambiguous: 2 or more other contacts found. No merge performed.');
return callback(null, { status: 'ambiguous_duplicates' });
}

// 3) Exactly 1 match → perform merge using direct API call
const primaryId = matchingIds[0];
console.log(`Merging enrolled contact (${enrolledId}) INTO contact (${primaryId})`);

try {
await hubspotClient.apiRequest({
method: 'POST',
path: '/crm/v3/objects/contacts/merge',
body: {
primaryObjectId: primaryId,
secondaryObjectId: enrolledId
}
});

console.log(' Merge request sent successfully (HubSpot may return 204 No Content).');
return callback(null, { status: 'merged', merged_into: primaryId });

} catch (mergeErr) {
console.error(' Merge API failed:', mergeErr.response?.body || mergeErr);
return callback(mergeErr);
}

} catch (err) {
console.error(' General error during merge process:', err);
return callback(err);
}
};


And here's the latest log:

Spoiler

Logs

2025-04-24T16:21:28.815Z INFO Searching for contacts with mobilephone = [hidden] 2025-04-24T16:21:29.135Z INFO Found 1 other contacts with same mobilephone. 2025-04-24T16:21:29.136Z INFO Merging enrolled contact (12479851) INTO contact (44946951) 2025-04-24T16:21:29.258Z INFO Merge request sent successfully (HubSpot may return 204 No Content).



0 Upvotes
1 Accepted solution
RubenBurdin
Solution
Top Contributor

Cannot merge two contacts using custom code in workflow

SOLVE

Hi @customtruck 

This might help you
• Use the POST /crm/v3/objects/contacts/merge endpoint (not the old “merge-two” v1 URL). Body must be: { "primaryObjectId": "123", "objectIdToMerge": "456" }—nothing else, no JSON array.
• Your private-app token needs crm.objects.contacts.write plus crm.objects.contacts.read; without write, HubSpot returns 200 in custom-code but silently drops the merge.
• v3 runs the merge async, so the 200 you log only means “request accepted.” It can take a few seconds—add a short delay or a polling step if you need to confirm inside the same workflow run.
• If you’re matching on mobilephone, be sure both records have an identical value in that exact internal property; HubSpot won’t merge on a partial match or on other phone fields.

• Custom-code boilerplate that works:

 

const hubspot = require('@hubspot/api-client');
exports.main = async (event, cb) => {
  const hs = new hubspot.Client({ accessToken: process.env.HUBSPOT_API_KEY });
  const primaryId = event.inputFields['hs_object_id'];        // enrolled contact
  const dupId     = event.inputFields['duplicate_id'];        // fetched via earlier search
  try {
    await hs.apiRequest({
      method: 'POST',
      path: '/crm/v3/objects/contacts/merge',
      body: { primaryObjectId: primaryId, objectIdToMerge: dupId }
    });
    cb({ outputFields: { merge_status: 'queued' }});
  } catch (e) {
    cb({ outputFields: { merge_status: `error: ${e.message}` }});
  }
};

 

 

(Replace duplicate_id with whatever search step you’re using.)

Once you have the right scopes and exact JSON body, contacts will merge every time—log shows 200, a few seconds later the duplicate VID disappears.

Hope it helps.

RubenB_0-1745515849609.png

Ruben Burdin 

Real-Time Data Sync Between any CRM or Database | Founder @Stacksync (YC W24) 

Did my answer help? Please mark it as a solution to help others find it too.

Ruben Burdin Ruben Burdin
HubSpot Advisor
Founder @ Stacksync
Real-Time Data Sync between any CRM and Database
Stacksync Banner

View solution in original post

2 Replies 2
RubenBurdin
Solution
Top Contributor

Cannot merge two contacts using custom code in workflow

SOLVE

Hi @customtruck 

This might help you
• Use the POST /crm/v3/objects/contacts/merge endpoint (not the old “merge-two” v1 URL). Body must be: { "primaryObjectId": "123", "objectIdToMerge": "456" }—nothing else, no JSON array.
• Your private-app token needs crm.objects.contacts.write plus crm.objects.contacts.read; without write, HubSpot returns 200 in custom-code but silently drops the merge.
• v3 runs the merge async, so the 200 you log only means “request accepted.” It can take a few seconds—add a short delay or a polling step if you need to confirm inside the same workflow run.
• If you’re matching on mobilephone, be sure both records have an identical value in that exact internal property; HubSpot won’t merge on a partial match or on other phone fields.

• Custom-code boilerplate that works:

 

const hubspot = require('@hubspot/api-client');
exports.main = async (event, cb) => {
  const hs = new hubspot.Client({ accessToken: process.env.HUBSPOT_API_KEY });
  const primaryId = event.inputFields['hs_object_id'];        // enrolled contact
  const dupId     = event.inputFields['duplicate_id'];        // fetched via earlier search
  try {
    await hs.apiRequest({
      method: 'POST',
      path: '/crm/v3/objects/contacts/merge',
      body: { primaryObjectId: primaryId, objectIdToMerge: dupId }
    });
    cb({ outputFields: { merge_status: 'queued' }});
  } catch (e) {
    cb({ outputFields: { merge_status: `error: ${e.message}` }});
  }
};

 

 

(Replace duplicate_id with whatever search step you’re using.)

Once you have the right scopes and exact JSON body, contacts will merge every time—log shows 200, a few seconds later the duplicate VID disappears.

Hope it helps.

RubenB_0-1745515849609.png

Ruben Burdin 

Real-Time Data Sync Between any CRM or Database | Founder @Stacksync (YC W24) 

Did my answer help? Please mark it as a solution to help others find it too.

Ruben Burdin Ruben Burdin
HubSpot Advisor
Founder @ Stacksync
Real-Time Data Sync between any CRM and Database
Stacksync Banner
customtruck
Participant

Cannot merge two contacts using custom code in workflow

SOLVE

Wow, I've been beating my head against this wall for hours and all it came down to was using "objectIdToMerge" instead of "secondaryObjectId", as you suggested. I am so relieved to finally have this working -- thank you so much for your timely assistance!

0 Upvotes