{"id":2045,"date":"2025-12-17T15:20:49","date_gmt":"2025-12-17T16:20:49","guid":{"rendered":"http:\/\/gogetmuscle.com\/?p=2045"},"modified":"2025-12-17T17:43:18","modified_gmt":"2025-12-17T17:43:18","slug":"two-way-sync-for-google-contact-labels-hubspot","status":"publish","type":"post","link":"http:\/\/gogetmuscle.com\/index.php\/2025\/12\/17\/two-way-sync-for-google-contact-labels-hubspot\/","title":{"rendered":"Two-Way Sync for Google Contact Labels & HubSpot"},"content":{"rendered":"

Hi everyone,<\/SPAN><\/P>

Like many of you, I\u2019ve struggled with a specific limitation in the HubSpot Google Contacts integration:\u00a0<\/SPAN><\/SPAN>It doesn\u2019t sync Labels (Contact Groups).<\/SPAN><\/STRONG><\/P>

If I tag a contact as “VIP” or “Partner” on my Android phone or Gmail, that information stays stuck in Google. HubSpot syncs the name and email but loses the segmentation. Conversely, if I segment people in HubSpot, I can’t easily push that back to a Google Contact Group on my phone.<\/SPAN><\/P>

The Solution:<\/SPAN><\/STRONG>
I created a Google Apps Script that works alongside the native HubSpot Data Sync. It creates a\u00a0<\/SPAN><\/SPAN>Two-Way Sync<\/SPAN><\/STRONG>\u00a0<\/SPAN>between Google Labels and a HubSpot Custom Field.<\/SPAN><\/P>

Full transparency: I built this script with the help of AI. I am sharing it because it works well for me and solves a major headache, but please test it carefully!<\/SPAN><\/P>


\u26a0\ufe0f<\/span>Important Warnings<\/SPAN><\/H3>
  1. Make a Backup:<\/SPAN><\/STRONG>\u00a0<\/SPAN>Before running this, export your Google Contacts to a CSV file just in case.<\/SPAN><\/P><\/LI>

  2. Beta Testing:<\/SPAN><\/STRONG>\u00a0<\/SPAN>This logic is sound, but every environment is different. Please test this on a small batch of contacts or a secondary account first if possible. Feedback is welcome!<\/SPAN><\/P><\/LI><\/OL>


    Step 1: Install the Google Script (Do this first!)<\/SPAN><\/H3>

    We install the script first so it can create the necessary fields in Google Contacts. This makes the HubSpot setup easier later.<\/SPAN><\/P>

    1. Go to\u00a0<\/SPAN><\/SPAN>script.google.com<\/SPAN><\/a>.<\/SPAN><\/P><\/LI>

    2. Click\u00a0<\/SPAN><\/SPAN>+ New Project<\/SPAN><\/STRONG>.<\/SPAN><\/P><\/LI>

    3. Name it\u00a0<\/SPAN><\/SPAN>HubSpot Label Sync<\/SPAN>.<\/SPAN><\/P><\/LI>

    4. Crucial:<\/SPAN><\/STRONG>\u00a0<\/SPAN>On the left sidebar, click the\u00a0<\/SPAN><\/SPAN>+<\/SPAN>\u00a0<\/SPAN>next to\u00a0<\/SPAN><\/SPAN>Services<\/SPAN><\/STRONG>, select\u00a0<\/SPAN><\/SPAN>People API<\/SPAN><\/STRONG>, and click\u00a0<\/SPAN><\/SPAN>Add<\/SPAN><\/STRONG>.<\/SPAN><\/P><\/LI>

    5. Paste the code below into the editor (delete any existing code).<\/SPAN><\/P><\/LI><\/OL>

      \u00a0<\/DIV><\/DIV>
      \/**
      \n * CONFIGURATION
      \n *\/<\/SPAN>
      \nconst<\/SPAN> CONFIG = {
      \n SYNC_FIELD_NAME<\/SPAN>: “HubSpot Labels”<\/SPAN>, \/\/ Must match your HubSpot Property Name later<\/SPAN>
      \n CHECKSUM_FIELD_NAME<\/SPAN>: “HS_Sync_Hash”<\/SPAN>, \/\/ Technical field, do not touch<\/SPAN>
      \n IGNORED_LABELS<\/SPAN>: [‘contactGroups\/myContacts’<\/SPAN>, ‘contactGroups\/starred’<\/SPAN>],
      \n SEPARATOR<\/SPAN>: “;”<\/SPAN>
      \n};<\/p>\n

      function<\/SPAN> installTrigger<\/SPAN>() <\/SPAN>{
      \n const<\/SPAN> triggers = ScriptApp.getProjectTriggers();
      \n for<\/SPAN> (let<\/SPAN> t of<\/SPAN> triggers) {
      \n if<\/SPAN> (t.getHandlerFunction() === ‘startSync’<\/SPAN>) {
      \n console<\/SPAN>.log(“Sync is already running!”<\/SPAN>);
      \n return<\/SPAN>;
      \n }
      \n }
      \n \/\/ Runs every 15 minutes to handle large lists safely<\/SPAN>
      \n ScriptApp.newTrigger(‘startSync’<\/SPAN>).timeBased().everyMinutes(15<\/SPAN>).create();
      \n console<\/SPAN>.log(\u2705<\/span> Installed! Sync runs every 15 mins.”<\/SPAN>);
      \n}<\/p>\n

      function<\/SPAN> startSync<\/SPAN>() <\/SPAN>{
      \n const<\/SPAN> groupMap = fetchGroupMap();
      \n let<\/SPAN> pageToken = null<\/SPAN>;
      \n do<\/SPAN> {
      \n const<\/SPAN> response = People.People.Connections.list(‘people\/me’<\/SPAN>, {
      \n personFields<\/SPAN>: ‘names,memberships,userDefined’<\/SPAN>,
      \n pageSize<\/SPAN>: 1000<\/SPAN>,
      \n pageToken<\/SPAN>: pageToken
      \n });
      \n const<\/SPAN> connections = response.connections || [];
      \n if<\/SPAN> (connections.length > 0<\/SPAN>) {
      \n for<\/SPAN> (const<\/SPAN> person of<\/SPAN> connections) {
      \n try<\/SPAN> { processContact(person, groupMap); }
      \n catch<\/SPAN> (e) { console<\/SPAN>.error(`Error: ${e.message}<\/SPAN>`<\/SPAN>); }
      \n }
      \n }
      \n pageToken = response.nextPageToken;
      \n } while<\/SPAN> (pageToken);
      \n}<\/p>\n

      function<\/SPAN> processContact<\/SPAN>(person, groupMap<\/SPAN>) <\/SPAN>{
      \n const<\/SPAN> resourceName = person.resourceName;
      \n const<\/SPAN> currentMemberships = person.memberships || [];
      \n const<\/SPAN> realLabelNames = [];<\/p>\n

      currentMemberships.forEach(mem<\/SPAN> =><\/SPAN> {
      \n if<\/SPAN> (mem.contactGroupMembership) {
      \n const<\/SPAN> res = mem.contactGroupMembership.contactGroupResourceName;
      \n const<\/SPAN> name = groupMap.idToName[res];
      \n if<\/SPAN> (name && !CONFIG.IGNORED_LABELS.includes(res)) realLabelNames.push(name);
      \n }
      \n });<\/p>\n

      realLabelNames.sort();
      \n const<\/SPAN> realLabelString = realLabelNames.join(CONFIG.SEPARATOR);<\/p>\n

      const<\/SPAN> userDefined = person.userDefined || [];
      \n let<\/SPAN> syncFieldVal = “”<\/SPAN>, checksumVal = “”<\/SPAN>;
      \n userDefined.forEach(f<\/SPAN> =><\/SPAN> {
      \n if<\/SPAN> (f.key === CONFIG.SYNC_FIELD_NAME) syncFieldVal = f.value;
      \n if<\/SPAN> (f.key === CONFIG.CHECKSUM_FIELD_NAME) checksumVal = f.value;
      \n });<\/p>\n

      const<\/SPAN> syncFieldArr = syncFieldVal ? syncFieldVal.split(CONFIG.SEPARATOR).map(s<\/SPAN> =><\/SPAN> s.trim()).filter(s<\/SPAN>=><\/SPAN>s).sort() : [];
      \n const<\/SPAN> normalizedSyncString = syncFieldArr.join(CONFIG.SEPARATOR);<\/p>\n

      const<\/SPAN> googleChanged = (realLabelString !== checksumVal);
      \n const<\/SPAN> hubspotChanged = (normalizedSyncString !== checksumVal);<\/p>\n

      if<\/SPAN> (!googleChanged && !hubspotChanged) return<\/SPAN>;<\/p>\n

      const<\/SPAN> contactToUpdate = { etag<\/SPAN>: person.etag };
      \n const<\/SPAN> fieldsToUpdate = [];<\/p>\n

      if<\/SPAN> (googleChanged) {
      \n const<\/SPAN> newUserDefined = rebuildUserDefined(userDefined, {
      \n [CONFIG.SYNC_FIELD_NAME]: realLabelString,
      \n [CONFIG.CHECKSUM_FIELD_NAME]: realLabelString
      \n });
      \n contactToUpdate.userDefined = newUserDefined;
      \n fieldsToUpdate.push(‘userDefined’<\/SPAN>);
      \n } else<\/SPAN> if<\/SPAN> (hubspotChanged) {
      \n const<\/SPAN> newUserDefined = rebuildUserDefined(userDefined, {
      \n [CONFIG.CHECKSUM_FIELD_NAME]: normalizedSyncString
      \n });
      \n contactToUpdate.userDefined = newUserDefined;
      \n fieldsToUpdate.push(‘userDefined’<\/SPAN>);<\/p>\n

      const<\/SPAN> newMemberships = [];
      \n currentMemberships.forEach(mem<\/SPAN> =><\/SPAN> {
      \n if<\/SPAN> (mem.contactGroupMembership) {
      \n const<\/SPAN> res = mem.contactGroupMembership.contactGroupResourceName;
      \n if<\/SPAN> (CONFIG.IGNORED_LABELS.includes(res)) newMemberships.push({ contactGroupMembership<\/SPAN>: { contactGroupResourceName<\/SPAN>: res } });
      \n }
      \n });
      \n syncFieldArr.forEach(labelName<\/SPAN> =><\/SPAN> {
      \n let<\/SPAN> groupId = groupMap.nameToId[labelName];
      \n if<\/SPAN> (!groupId) {
      \n groupId = People.ContactGroups.create({ contactGroup<\/SPAN>: { name<\/SPAN>: labelName } }).resourceName;
      \n groupMap.nameToId[labelName] = groupId;
      \n }
      \n newMemberships.push({ contactGroupMembership<\/SPAN>: { contactGroupResourceName<\/SPAN>: groupId } });
      \n });
      \n contactToUpdate.memberships = newMemberships;
      \n fieldsToUpdate.push(‘memberships’<\/SPAN>);
      \n }<\/p>\n

      if<\/SPAN> (fieldsToUpdate.length > 0<\/SPAN>) {
      \n People.People.updateContact(contactToUpdate, resourceName, { updatePersonFields<\/SPAN>: fieldsToUpdate.join(‘,’<\/SPAN>) });
      \n }
      \n}<\/p>\n

      function<\/SPAN> fetchGroupMap<\/SPAN>() <\/SPAN>{
      \n let<\/SPAN> pageToken = null<\/SPAN>;
      \n const<\/SPAN> map = { nameToId<\/SPAN>: {}, idToName<\/SPAN>: {} };
      \n do<\/SPAN> {
      \n const<\/SPAN> response = People.ContactGroups.list({ groupFields<\/SPAN>: ‘name’<\/SPAN>, pageSize<\/SPAN>: 1000<\/SPAN>, pageToken<\/SPAN>: pageToken });
      \n (response.contactGroups || []).forEach(g<\/SPAN> =><\/SPAN> {
      \n const<\/SPAN> name = g.formattedName || g.name;
      \n const<\/SPAN> id = g.resourceName;
      \n if<\/SPAN> (name && id) { map.nameToId[name] = id; map.idToName[id] = name; }
      \n });
      \n pageToken = response.nextPageToken;
      \n } while<\/SPAN> (pageToken);
      \n return<\/SPAN> map;
      \n}<\/p>\n

      function<\/SPAN> rebuildUserDefined<\/SPAN>(cur, updates<\/SPAN>) <\/SPAN>{
      \n let<\/SPAN> arr = cur ? […cur] : [];
      \n for<\/SPAN> (const<\/SPAN> [key, val] of<\/SPAN> Object<\/SPAN>.entries(updates)) {
      \n const<\/SPAN> idx = arr.findIndex(f<\/SPAN> =><\/SPAN> f.key === key);
      \n if<\/SPAN> (idx > –1<\/SPAN>) arr[idx].value = val; else<\/SPAN> arr.push({ key<\/SPAN>: key, value<\/SPAN>: val });
      \n }
      \n return<\/SPAN> arr;
      \n}<\/PRE><\/DIV><\/DIV><\/DIV><\/DIV>

      Step 2: Initialize the Data<\/SPAN><\/H3>
      1. In the Script Editor, select the function\u00a0<\/SPAN><\/SPAN>installTrigger<\/SPAN><\/STRONG>\u00a0<\/SPAN>from the toolbar and click\u00a0<\/SPAN><\/SPAN>Run<\/SPAN><\/STRONG>.<\/SPAN><\/P><\/LI>

      2. Accept the permissions. (You may need to click “Advanced” > “Go to Script (Unsafe)”).<\/SPAN><\/P><\/LI>

      3. Once that is done, select\u00a0<\/SPAN><\/SPAN>startSync<\/SPAN><\/STRONG>\u00a0<\/SPAN>and click\u00a0<\/SPAN><\/SPAN>Run<\/SPAN><\/STRONG>\u00a0<\/SPAN>manually once.<\/SPAN><\/P>

        • This will look at your current Google Contacts and write your existing labels into a new custom field called “HubSpot Labels”.<\/SPAN><\/P><\/LI><\/UL><\/LI><\/OL>

          Step 3: Configure HubSpot<\/SPAN><\/H3>

          Now that your Google Contacts have the field, let’s set up the HubSpot sync.<\/SPAN><\/P>

          1. Go to\u00a0<\/SPAN><\/SPAN>App Marketplace > Google Contacts<\/SPAN><\/STRONG>.<\/SPAN><\/LI>
          2. Set up the sync (or edit existing).<\/SPAN><\/P><\/LI>

          3. Go to the\u00a0<\/SPAN><\/SPAN>Field Mappings<\/SPAN><\/STRONG>\u00a0<\/SPAN>tab and add a new mapping:<\/SPAN><\/P>

            • Google Contacts<\/STRONG> side: add a custom field and name it exactly “HubSpot Labels”. It will look like you’re creating a new field, but it’ll actually pick up the existing field.<\/SPAN><\/LI>
            • HubSpot Side:<\/SPAN><\/STRONG>\u00a0Create a new single-line text field<\/SPAN><\/SPAN>.<\/SPAN><\/P><\/LI><\/UL><\/LI>

            • Turn on the sync.<\/SPAN><\/P><\/LI><\/OL>

              How it works<\/SPAN><\/H3>
              • Google -> HubSpot:<\/SPAN><\/STRONG>\u00a0<\/SPAN>The script detects you added a Label (e.g., “VIP”), writes “VIP” to the text field, and HubSpot syncs that text.<\/SPAN><\/P><\/LI>

              • HubSpot -> Google:<\/SPAN><\/STRONG>\u00a0<\/SPAN>You write “VIP” in the HubSpot text field, the sync pushes it to Google, and the script detects the change and automatically creates\/adds the “VIP” Contact Group.<\/SPAN><\/P><\/LI><\/UL>

                Hope this helps! Let me know if you run into any issues.<\/SPAN><\/P><\/p>\n","protected":false},"excerpt":{"rendered":"

                Hi everyone,Like many of you, I\u2019ve struggled with a specific limitation in the HubSpot Google Contacts integration:\u00a0It doesn\u2019t sync Labels (Contact Groups).If I tag a contact as “VIP” or “Partner” on my Android phone or Gmail, that information stays stuck in Google. HubSpot syncs the name and email but loses the segmentation. Conversely, if I […]<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":[],"categories":[14],"tags":[],"_links":{"self":[{"href":"http:\/\/gogetmuscle.com\/index.php\/wp-json\/wp\/v2\/posts\/2045"}],"collection":[{"href":"http:\/\/gogetmuscle.com\/index.php\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"http:\/\/gogetmuscle.com\/index.php\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"http:\/\/gogetmuscle.com\/index.php\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"http:\/\/gogetmuscle.com\/index.php\/wp-json\/wp\/v2\/comments?post=2045"}],"version-history":[{"count":1,"href":"http:\/\/gogetmuscle.com\/index.php\/wp-json\/wp\/v2\/posts\/2045\/revisions"}],"predecessor-version":[{"id":2046,"href":"http:\/\/gogetmuscle.com\/index.php\/wp-json\/wp\/v2\/posts\/2045\/revisions\/2046"}],"wp:attachment":[{"href":"http:\/\/gogetmuscle.com\/index.php\/wp-json\/wp\/v2\/media?parent=2045"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"http:\/\/gogetmuscle.com\/index.php\/wp-json\/wp\/v2\/categories?post=2045"},{"taxonomy":"post_tag","embeddable":true,"href":"http:\/\/gogetmuscle.com\/index.php\/wp-json\/wp\/v2\/tags?post=2045"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}