CRM

JRaaschou
Participant | Gold Partner
Participant | Gold Partner

Associate listings and deals to tickets

SOLVE

Hi everyone! 

 

I’m working with a client in the housing industry who uses the Listing and Deal objects to manage their properties and sales. Here's the scenario:

When a 
contact is associated with a listing and a deal, and then raises a ticket (e.g. a complaint about the floor in the house), we want the associated listing and deal to automatically be linked to that ticket. This way, the service rep has immediate context and a full overview in the help desk. (and they can follow potential issues on that specific listing even though the buyer sell the house down the road)

So far, I haven’t found a way to do this with native workflows. Has anyone found a workaround or integration that could support this kind of cross-object association?
Appreciate any insights!

0 Upvotes
2 Accepted solutions
karstenkoehler
Solution
Hall of Famer | Partner
Hall of Famer | Partner

Associate listings and deals to tickets

SOLVE

@JRaaschou yes, did you review the workflow action I shared in my previous reply?

 

You would create a ticket-based workflow, use the 'Create associations' workflow action and would then have to find an identifier that is shared between the ticket, the listing, and the deal.

 

How exactly that would be done depends on your setup. Via additional workflows or sync properties, you would have to make sure that these records have a property where they share a value. For example, this could be a listing name or ID that is also present on the deal and also present on the ticket.

Karsten Köhler
HubSpot Freelancer | RevOps & CRM Consultant | Community Hall of Famer

Beratungstermin mit Karsten vereinbaren

 

Did my post help answer your query? Help the community by marking it as a solution.

View solution in original post

0 Upvotes
JRaaschou
Solution
Participant | Gold Partner
Participant | Gold Partner

Associate listings and deals to tickets

SOLVE

I ran into the same challenge – having to build manual workflows just to get the Record ID onto the ticket. The process of going from listings and deals → to the contact → and then from the contact → to the ticket is just unnecessarily complex.

Honestly, it’s mind-boggling that HubSpot doesn’t support something this basic out of the box. The whole point of associations starts to feel pretty pointless when you can’t easily cross-associate objects.

Anyway, Claude 4.0 came to the rescue with a custom API call through Operations Hub. 🙌
So if anyone else is stuck in the same situation – here’s the code, totally free to use: 😄

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

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

  const ticketId = event.inputFields['hs_ticket_id'];
  const contactId = event.inputFields['hs_contact_id'];
  const LISTINGS_OBJECT_TYPE_ID = '0-420';

  try {
    console.log(`🎫 Ticket ID: ${ticketId}`);
    console.log(`👤 Contact ID: ${contactId}`);

    if (!contactId) {
      console.log("️No contact ID provided.");
      return { success: false, error: "No contact ID" };
    }

    // LØSNING 1: Hent deals
    console.log("🔍 Fetching deals using SDK...");
    let dealIds = [];
    try {
      const dealsResponse = await hubspotClient.crm.associations.v4.basicApi.getPage(
        'contacts', 
        contactId, 
        'deals'
      );
      dealIds = dealsResponse?.results?.map(r => r.toObjectId) ?? [];
      console.log(`💼 Found ${dealIds.length} deals`);
    } catch (dealError) {
      console.log("⚠️ SDK deals failed, trying fallback...", dealError.message);
      try {
        const contactDeals = await hubspotClient.crm.contacts.associationsApi.getAll(
          contactId,
          'deals'
        );
        dealIds = contactDeals?.results?.map(r => r.id) ?? [];
      } catch (basicError) {
        console.log(" Basic deals API also failed:", basicError.message);
      }
    }
    console.log(`💼 Final deals count: ${dealIds.length}`);

    // LØSNING 2: Hent listings med flere strategies
    console.log("🔍 Fetching listings with multiple strategies...");
    let listingIds = [];
    
    // Strategy 1: Forskellige object identifiers
    const listingIdentifiers = [LISTINGS_OBJECT_TYPE_ID, 'listings', '2-420', 'p420'];
    
    for (const identifier of listingIdentifiers) {
      if (listingIds.length > 0) break;
      
      console.log(`🔄 Trying identifier: ${identifier}`);
      try {
        const listingsResponse = await hubspotClient.crm.associations.v4.basicApi.getPage(
          'contacts',
          contactId,
          identifier
        );
        listingIds = listingsResponse?.results?.map(r => r.toObjectId) ?? [];
        if (listingIds.length > 0) {
          console.log(` Found ${listingIds.length} listings with identifier ${identifier}`);
          break;
        }
      } catch (error) {
        console.log(` Identifier ${identifier} failed:`, error.message);
      }
    }
    
    // Strategy 2: v3 associations
    if (listingIds.length === 0) {
      console.log("🔄 Trying v3 associations API...");
      try {
        const v3Response = await hubspotClient.crm.contacts.associationsApi.getAll(
          contactId,
          LISTINGS_OBJECT_TYPE_ID
        );
        listingIds = v3Response?.results?.map(r => r.id) ?? [];
        console.log(`📋 v3 found ${listingIds.length} listings`);
      } catch (v3Error) {
        console.log(" v3 associations failed:", v3Error.message);
      }
    }
    
    // Strategy 3: Direct search
    if (listingIds.length === 0) {
      console.log("🔄 Searching for listings directly...");
      
      const searchFilters = [
        { propertyName: 'associations.contact', operator: 'EQ', value: contactId },
        { propertyName: 'hs_associated_contact_id', operator: 'EQ', value: contactId },
        { propertyName: 'contact_id', operator: 'EQ', value: contactId }
      ];
      
      for (const filter of searchFilters) {
        if (listingIds.length > 0) break;
        
        try {
          const searchResponse = await hubspotClient.crm.objects.searchApi.doSearch(LISTINGS_OBJECT_TYPE_ID, {
            filterGroups: [{ filters: [filter] }],
            limit: 100,
            properties: ['hs_object_id']
          });
          listingIds = searchResponse?.results?.map(r => r.id) ?? [];
          if (listingIds.length > 0) {
            console.log(` Found ${listingIds.length} listings with search`);
            break;
          }
        } catch (searchError) {
          console.log(` Search failed:`, searchError.message);
        }
      }
    }

    console.log(`📋 Final listings count: ${listingIds.length}`);

    // Debug hvis ingen listings findes
    if (listingIds.length === 0) {
      console.log("🔍 No listings found. Getting object properties...");
      try {
        const propertiesResponse = await hubspotClient.crm.properties.coreApi.getAll(LISTINGS_OBJECT_TYPE_ID);
        const associationProps = propertiesResponse?.results?.filter(p => 
          p.name.includes('contact') || p.name.includes('association')
        ) ?? [];
        console.log(`📋 Available contact properties:`, associationProps.map(p => p.name));
      } catch (propsError) {
        console.log(" Properties fetch failed:", propsError.message);
      }
    }

    // LØSNING 3: Lav associations
    console.log(`🔗 Creating associations...`);

    // Associate deals
    for (const dealId of dealIds) {
      try {
        await hubspotClient.crm.associations.v4.basicApi.create(
          'tickets',
          ticketId,
          'deals', 
          dealId,
          [{
            associationCategory: 'HUBSPOT_DEFINED',
            associationTypeId: 28
          }]
        );
        console.log(` Associated deal ${dealId}`);
      } catch (assocError) {
        console.log(` Failed to associate deal ${dealId}:`, assocError.message);
      }
    }

    // Associate listings - med omfattende debugging
    for (const listingId of listingIds) {
      console.log(`🔍 Attempting to associate listing ${listingId} with ticket ${ticketId}`);
      
      // Først - tjek om listing faktisk eksisterer
      try {
        const listingCheck = await hubspotClient.crm.objects.basicApi.getById(LISTINGS_OBJECT_TYPE_ID, listingId);
        console.log(`📋 Listing ${listingId} exists:`, listingCheck.id);
      } catch (checkError) {
        console.log(` Listing ${listingId} doesn't exist:`, checkError.message);
        continue;
      }

      // Find korrekte association type IDs for listings
      try {
        console.log(`🔍 Getting association types between tickets and ${LISTINGS_OBJECT_TYPE_ID}...`);
        const assocTypes = await hubspotClient.crm.associations.schemaApi.getAll('tickets', LISTINGS_OBJECT_TYPE_ID);
        console.log(`📋 Available association types:`, JSON.stringify(assocTypes?.results?.map(r => ({
          id: r.typeId,
          label: r.label,
          name: r.name
        })), null, 2));
        
        // Brug den første tilgængelige association type
        const validAssocType = assocTypes?.results?.[0]?.typeId || 1;
        console.log(`🎯 Using association type ID: ${validAssocType}`);
        
        // Prøv association med korrekt type
        await hubspotClient.crm.associations.v4.basicApi.create(
          'tickets',
          ticketId,
          LISTINGS_OBJECT_TYPE_ID,
          listingId,
          [{
            associationCategory: 'HUBSPOT_DEFINED',
            associationTypeId: validAssocType
          }]
        );
        console.log(` Associated listing ${listingId} with correct type ${validAssocType}`);
        
        // Verificer association blev lavet
        const verification = await hubspotClient.crm.associations.v4.basicApi.getPage(
          'tickets',
          ticketId,
          LISTINGS_OBJECT_TYPE_ID
        );
        const associatedListings = verification?.results?.map(r => r.toObjectId) || [];
        console.log(`🔍 Verification - ticket ${ticketId} is now associated with listings:`, associatedListings);
        
        if (associatedListings.includes(listingId)) {
          console.log(` CONFIRMED: Listing ${listingId} is properly associated!`);
        } else {
          console.log(` FAILED: Listing ${listingId} association not found in verification`);
        }
        
      } catch (schemaError) {
        console.log(` Schema fetch failed, trying manual approaches:`, schemaError.message);
        
        // Fallback - prøv forskellige association approaches
        const approaches = [
          { 
            name: 'Batch API with generic type',
            action: () => hubspotClient.crm.associations.batchApi.create('tickets', LISTINGS_OBJECT_TYPE_ID, {
              inputs: [{
                from: { id: ticketId },
                to: { id: listingId },
                type: 'ticket_to_listing'
              }]
            })
          },
          {
            name: 'Direct API v3 style',
            action: () => hubspotClient.apiRequest({
              method: 'PUT',
              path: `/crm/v3/objects/tickets/${ticketId}/associations/${LISTINGS_OBJECT_TYPE_ID}/${listingId}/ticket_to_listing`
            })
          },
          {
            name: 'Direct API v4 style',
            action: () => hubspotClient.apiRequest({
              method: 'PUT',
              path: `/crm/v4/objects/tickets/${ticketId}/associations/${LISTINGS_OBJECT_TYPE_ID}/${listingId}`,
              body: {
                associationCategory: 'HUBSPOT_DEFINED',
                associationTypeId: 1
              }
            })
          }
        ];
        
        for (const approach of approaches) {
          try {
            console.log(`🔄 Trying ${approach.name}...`);
            await approach.action();
            console.log(` ${approach.name} succeeded`);
            
            // Test verification igen
            try {
              const verification2 = await hubspotClient.crm.associations.v4.basicApi.getPage(
                'tickets',
                ticketId,
                LISTINGS_OBJECT_TYPE_ID
              );
              const associatedListings2 = verification2?.results?.map(r => r.toObjectId) || [];
              if (associatedListings2.includes(listingId)) {
                console.log(` CONFIRMED: ${approach.name} worked! Listing is associated.`);
                break;
              } else {
                console.log(` ${approach.name} didn't create visible association`);
              }
            } catch (verifyError) {
              console.log(` Verification failed after ${approach.name}`);
            }
            
          } catch (approachError) {
            console.log(` ${approach.name} failed:`, approachError.message);
          }
        }
      }
    }

    const result = {
      success: true,
      associatedDeals: dealIds.length,
      associatedListings: listingIds.length,
      ticketId: ticketId
    };
    
    console.log(` Final result:`, JSON.stringify(result));
    return result;

  } catch (err) {
    console.error("🚫 Global Error:", err.message);
    return {
      success: false,
      error: err.message
    };
  }
};




View solution in original post

0 Upvotes
4 Replies 4
karstenkoehler
Hall of Famer | Partner
Hall of Famer | Partner

Associate listings and deals to tickets

SOLVE

Hi @JRaaschou,

 

This can be achieved with the "Create associations" workflow action:

 

karstenkoehler_0-1747980063813.png

 

The challenge is that you need a value that is shared between listing, deal, and ticket so you can match them up / associate them.

 

Best regards!

Karsten Köhler
HubSpot Freelancer | RevOps & CRM Consultant | Community Hall of Famer

Beratungstermin mit Karsten vereinbaren

 

Did my post help answer your query? Help the community by marking it as a solution.

0 Upvotes
JRaaschou
Participant | Gold Partner
Participant | Gold Partner

Associate listings and deals to tickets

SOLVE

The specific issue is that the contact that raises the ticket is associated to the deal and listing and i would like the ticket to automatically be associated to the deal and listing.

Is that doable somehow?

0 Upvotes
karstenkoehler
Solution
Hall of Famer | Partner
Hall of Famer | Partner

Associate listings and deals to tickets

SOLVE

@JRaaschou yes, did you review the workflow action I shared in my previous reply?

 

You would create a ticket-based workflow, use the 'Create associations' workflow action and would then have to find an identifier that is shared between the ticket, the listing, and the deal.

 

How exactly that would be done depends on your setup. Via additional workflows or sync properties, you would have to make sure that these records have a property where they share a value. For example, this could be a listing name or ID that is also present on the deal and also present on the ticket.

Karsten Köhler
HubSpot Freelancer | RevOps & CRM Consultant | Community Hall of Famer

Beratungstermin mit Karsten vereinbaren

 

Did my post help answer your query? Help the community by marking it as a solution.

0 Upvotes
JRaaschou
Solution
Participant | Gold Partner
Participant | Gold Partner

Associate listings and deals to tickets

SOLVE

I ran into the same challenge – having to build manual workflows just to get the Record ID onto the ticket. The process of going from listings and deals → to the contact → and then from the contact → to the ticket is just unnecessarily complex.

Honestly, it’s mind-boggling that HubSpot doesn’t support something this basic out of the box. The whole point of associations starts to feel pretty pointless when you can’t easily cross-associate objects.

Anyway, Claude 4.0 came to the rescue with a custom API call through Operations Hub. 🙌
So if anyone else is stuck in the same situation – here’s the code, totally free to use: 😄

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

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

  const ticketId = event.inputFields['hs_ticket_id'];
  const contactId = event.inputFields['hs_contact_id'];
  const LISTINGS_OBJECT_TYPE_ID = '0-420';

  try {
    console.log(`🎫 Ticket ID: ${ticketId}`);
    console.log(`👤 Contact ID: ${contactId}`);

    if (!contactId) {
      console.log("️No contact ID provided.");
      return { success: false, error: "No contact ID" };
    }

    // LØSNING 1: Hent deals
    console.log("🔍 Fetching deals using SDK...");
    let dealIds = [];
    try {
      const dealsResponse = await hubspotClient.crm.associations.v4.basicApi.getPage(
        'contacts', 
        contactId, 
        'deals'
      );
      dealIds = dealsResponse?.results?.map(r => r.toObjectId) ?? [];
      console.log(`💼 Found ${dealIds.length} deals`);
    } catch (dealError) {
      console.log("⚠️ SDK deals failed, trying fallback...", dealError.message);
      try {
        const contactDeals = await hubspotClient.crm.contacts.associationsApi.getAll(
          contactId,
          'deals'
        );
        dealIds = contactDeals?.results?.map(r => r.id) ?? [];
      } catch (basicError) {
        console.log(" Basic deals API also failed:", basicError.message);
      }
    }
    console.log(`💼 Final deals count: ${dealIds.length}`);

    // LØSNING 2: Hent listings med flere strategies
    console.log("🔍 Fetching listings with multiple strategies...");
    let listingIds = [];
    
    // Strategy 1: Forskellige object identifiers
    const listingIdentifiers = [LISTINGS_OBJECT_TYPE_ID, 'listings', '2-420', 'p420'];
    
    for (const identifier of listingIdentifiers) {
      if (listingIds.length > 0) break;
      
      console.log(`🔄 Trying identifier: ${identifier}`);
      try {
        const listingsResponse = await hubspotClient.crm.associations.v4.basicApi.getPage(
          'contacts',
          contactId,
          identifier
        );
        listingIds = listingsResponse?.results?.map(r => r.toObjectId) ?? [];
        if (listingIds.length > 0) {
          console.log(` Found ${listingIds.length} listings with identifier ${identifier}`);
          break;
        }
      } catch (error) {
        console.log(` Identifier ${identifier} failed:`, error.message);
      }
    }
    
    // Strategy 2: v3 associations
    if (listingIds.length === 0) {
      console.log("🔄 Trying v3 associations API...");
      try {
        const v3Response = await hubspotClient.crm.contacts.associationsApi.getAll(
          contactId,
          LISTINGS_OBJECT_TYPE_ID
        );
        listingIds = v3Response?.results?.map(r => r.id) ?? [];
        console.log(`📋 v3 found ${listingIds.length} listings`);
      } catch (v3Error) {
        console.log(" v3 associations failed:", v3Error.message);
      }
    }
    
    // Strategy 3: Direct search
    if (listingIds.length === 0) {
      console.log("🔄 Searching for listings directly...");
      
      const searchFilters = [
        { propertyName: 'associations.contact', operator: 'EQ', value: contactId },
        { propertyName: 'hs_associated_contact_id', operator: 'EQ', value: contactId },
        { propertyName: 'contact_id', operator: 'EQ', value: contactId }
      ];
      
      for (const filter of searchFilters) {
        if (listingIds.length > 0) break;
        
        try {
          const searchResponse = await hubspotClient.crm.objects.searchApi.doSearch(LISTINGS_OBJECT_TYPE_ID, {
            filterGroups: [{ filters: [filter] }],
            limit: 100,
            properties: ['hs_object_id']
          });
          listingIds = searchResponse?.results?.map(r => r.id) ?? [];
          if (listingIds.length > 0) {
            console.log(` Found ${listingIds.length} listings with search`);
            break;
          }
        } catch (searchError) {
          console.log(` Search failed:`, searchError.message);
        }
      }
    }

    console.log(`📋 Final listings count: ${listingIds.length}`);

    // Debug hvis ingen listings findes
    if (listingIds.length === 0) {
      console.log("🔍 No listings found. Getting object properties...");
      try {
        const propertiesResponse = await hubspotClient.crm.properties.coreApi.getAll(LISTINGS_OBJECT_TYPE_ID);
        const associationProps = propertiesResponse?.results?.filter(p => 
          p.name.includes('contact') || p.name.includes('association')
        ) ?? [];
        console.log(`📋 Available contact properties:`, associationProps.map(p => p.name));
      } catch (propsError) {
        console.log(" Properties fetch failed:", propsError.message);
      }
    }

    // LØSNING 3: Lav associations
    console.log(`🔗 Creating associations...`);

    // Associate deals
    for (const dealId of dealIds) {
      try {
        await hubspotClient.crm.associations.v4.basicApi.create(
          'tickets',
          ticketId,
          'deals', 
          dealId,
          [{
            associationCategory: 'HUBSPOT_DEFINED',
            associationTypeId: 28
          }]
        );
        console.log(` Associated deal ${dealId}`);
      } catch (assocError) {
        console.log(` Failed to associate deal ${dealId}:`, assocError.message);
      }
    }

    // Associate listings - med omfattende debugging
    for (const listingId of listingIds) {
      console.log(`🔍 Attempting to associate listing ${listingId} with ticket ${ticketId}`);
      
      // Først - tjek om listing faktisk eksisterer
      try {
        const listingCheck = await hubspotClient.crm.objects.basicApi.getById(LISTINGS_OBJECT_TYPE_ID, listingId);
        console.log(`📋 Listing ${listingId} exists:`, listingCheck.id);
      } catch (checkError) {
        console.log(` Listing ${listingId} doesn't exist:`, checkError.message);
        continue;
      }

      // Find korrekte association type IDs for listings
      try {
        console.log(`🔍 Getting association types between tickets and ${LISTINGS_OBJECT_TYPE_ID}...`);
        const assocTypes = await hubspotClient.crm.associations.schemaApi.getAll('tickets', LISTINGS_OBJECT_TYPE_ID);
        console.log(`📋 Available association types:`, JSON.stringify(assocTypes?.results?.map(r => ({
          id: r.typeId,
          label: r.label,
          name: r.name
        })), null, 2));
        
        // Brug den første tilgængelige association type
        const validAssocType = assocTypes?.results?.[0]?.typeId || 1;
        console.log(`🎯 Using association type ID: ${validAssocType}`);
        
        // Prøv association med korrekt type
        await hubspotClient.crm.associations.v4.basicApi.create(
          'tickets',
          ticketId,
          LISTINGS_OBJECT_TYPE_ID,
          listingId,
          [{
            associationCategory: 'HUBSPOT_DEFINED',
            associationTypeId: validAssocType
          }]
        );
        console.log(` Associated listing ${listingId} with correct type ${validAssocType}`);
        
        // Verificer association blev lavet
        const verification = await hubspotClient.crm.associations.v4.basicApi.getPage(
          'tickets',
          ticketId,
          LISTINGS_OBJECT_TYPE_ID
        );
        const associatedListings = verification?.results?.map(r => r.toObjectId) || [];
        console.log(`🔍 Verification - ticket ${ticketId} is now associated with listings:`, associatedListings);
        
        if (associatedListings.includes(listingId)) {
          console.log(` CONFIRMED: Listing ${listingId} is properly associated!`);
        } else {
          console.log(` FAILED: Listing ${listingId} association not found in verification`);
        }
        
      } catch (schemaError) {
        console.log(` Schema fetch failed, trying manual approaches:`, schemaError.message);
        
        // Fallback - prøv forskellige association approaches
        const approaches = [
          { 
            name: 'Batch API with generic type',
            action: () => hubspotClient.crm.associations.batchApi.create('tickets', LISTINGS_OBJECT_TYPE_ID, {
              inputs: [{
                from: { id: ticketId },
                to: { id: listingId },
                type: 'ticket_to_listing'
              }]
            })
          },
          {
            name: 'Direct API v3 style',
            action: () => hubspotClient.apiRequest({
              method: 'PUT',
              path: `/crm/v3/objects/tickets/${ticketId}/associations/${LISTINGS_OBJECT_TYPE_ID}/${listingId}/ticket_to_listing`
            })
          },
          {
            name: 'Direct API v4 style',
            action: () => hubspotClient.apiRequest({
              method: 'PUT',
              path: `/crm/v4/objects/tickets/${ticketId}/associations/${LISTINGS_OBJECT_TYPE_ID}/${listingId}`,
              body: {
                associationCategory: 'HUBSPOT_DEFINED',
                associationTypeId: 1
              }
            })
          }
        ];
        
        for (const approach of approaches) {
          try {
            console.log(`🔄 Trying ${approach.name}...`);
            await approach.action();
            console.log(` ${approach.name} succeeded`);
            
            // Test verification igen
            try {
              const verification2 = await hubspotClient.crm.associations.v4.basicApi.getPage(
                'tickets',
                ticketId,
                LISTINGS_OBJECT_TYPE_ID
              );
              const associatedListings2 = verification2?.results?.map(r => r.toObjectId) || [];
              if (associatedListings2.includes(listingId)) {
                console.log(` CONFIRMED: ${approach.name} worked! Listing is associated.`);
                break;
              } else {
                console.log(` ${approach.name} didn't create visible association`);
              }
            } catch (verifyError) {
              console.log(` Verification failed after ${approach.name}`);
            }
            
          } catch (approachError) {
            console.log(` ${approach.name} failed:`, approachError.message);
          }
        }
      }
    }

    const result = {
      success: true,
      associatedDeals: dealIds.length,
      associatedListings: listingIds.length,
      ticketId: ticketId
    };
    
    console.log(` Final result:`, JSON.stringify(result));
    return result;

  } catch (err) {
    console.error("🚫 Global Error:", err.message);
    return {
      success: false,
      error: err.message
    };
  }
};




0 Upvotes