Skip to content

Creating HubSpot Quotes with Custom Code

In our journey through HubSpot's custom-coded actions, we've already tackled how to verify email addresses with Kickbox and cloned deals and line items. If you're just joining us, you might want to check out those articles before diving in here:

  1. Custom Coded Actions: Kickbox Verification
  2. Custom Coded Actions: Clone Deal
  3. Custom Coded Actions: Clone Line Items

Today, we're going to explore another fascinating realm of HubSpot's custom-coded actions: creating and managing quotes. This process involves a bit more code than our previous articles, but don't worry - we'll break it down piece by piece to make it digestible.

A Brief Overview

Let's embark on a journey to simplify and modernize the process of generating quotes in HubSpot's CRM through the latest advancements in their API and SDK capabilities. Our mission is crystal clear: to seamlessly create a new quote, enrich it with precise details, associate it with the necessary CRM entities, and ensure its status is accurately reflected. The road ahead involves a series of well-defined steps:

  1. Importing essential libraries.
  2. Initializing the HubSpot API client.
  3. Capturing input data.
  4. Fetching and constructing associations.
  5. Assigning properties to the quote.
  6. Generating the quote.
  7. Transitioning the quote to the desired status.

Let's delve deeper into each step, ensuring clarity and efficiency at every turn.

Navigating Through the Code

Setting the QuoteTemplateID

This is probably the most tricky part of the code, as it requires an API call to get the template ID.
we decided to use Postman to make a GET request to /crm/v4/objects/quote_template?properties=hs_name,hs_active
That will retrieve all active quote templates. See also the HubSpot quotes developer documentation for more info about this.

We will use the QuoteTemplateID later in the code, but have the preference to set all 'settings' at the top of the code.

const QuoteTemplateID = 157057354080;

Calling the libraries and connecting the client

The first step in our script is to import the necessary libraries. In this case, we're importing the HubSpot API client.

Next, we instantiate a new Hubspot API client. This client will be used to make requests to HubSpot's API endpoints. 
We're authenticating the client using the same access token we previously used and stored in a secret: PrivateAppDealActions.

const hubspot = require('@hubspot/api-client');
const hubspotClient = new hubspot.Client({
    accessToken: process.env.PrivateAppDealActions
});

Retrieving the Input Fields

In this step, we're retrieving the input fields that will be used in the script. Please note that we set the inputfields first in the designated fields above the custom code:

InputFields-Quote

These variables will be used later when creating and managing the quote.

    const hs_object_id = event.inputFields['hs_object_id'];
    const dealname = event.inputFields['dealname']; 
    const hubspot_owner_id = event.inputFields['hubspot_owner_id']; 
    const deal_currency_code = event.inputFields['deal_currency_code']; 
    const renew_date = event.inputFields['renew_date'];

Efficient Association Handling: helper function

While we write this, it should be clear that creating a quote boils down to associations:
Associate the quote to all line items in the deal, the primary company, the contacts, but also to the template. To make this more efficient we will write a simple function that creates the associations for us. This reduces the chance of us making a typo and increases efficiency.

    // Helper function to create associations
    function createAssociations(associatedItems, associationCategory, associationTypeId) {
      return associatedItems.map(item => ({
        to: { id: item },
        types: [
          {
            associationCategory,
            associationTypeId,
          },
        ],
      }));
    }


And an similar function to fetch the associations

  	// Helper function to fetch associations
    async function fetchAssociations(fromObjectType, fromObjectId, toObjectType) {
        try {
            const response = await hubspotClient.crm.associations.v4.basicApi.getPage(fromObjectType, fromObjectId, toObjectType, undefined, 100);
            return response.results.map(({ toObjectId }) => String(toObjectId));
        } catch (error) {
            console.error(`Failed to fetch ${toObjectType} associations for ${fromObjectType} ${fromObjectId}:`, error.message);
            return [];
        }
    }

 

This makes retrieving the associations simple. We will execute the functions in paralel for efficiencly purposes

    const [associatedLineItems, associatedContacts, associatedCompanies] = await Promise.all([
        fetchAssociations('deals', hs_object_id, 'line_items'),
        fetchAssociations('deals', hs_object_id, 'contacts'),
        fetchAssociations('deals', hs_object_id, 'companies'),
    ]);

Each of these steps retrieves the respective associations and maps them to an array of strings.

But first we need to retrieve the AssociationsTypeID's for to set the association. Unlike with the line items to deals this is not simply listed in the documentation and you need to do another API call to retrieve them. Full explanation on how to do that is in the documentation. Spoiler: it is a simple postmen request. If you already set up the authentication you can just amend the endpoint.

    const QuoteToTemplateAssociationTypeID = 286
    const QuoteToDealsAssociationTypeID = 64;
    const QuoteToLineitemsAssociationTypeID = 67;  
    const QuoteToContactsAssociationTypeID = 69;   
    const QuoteToCompaniesAssociationTypeID = 71;  

And with all the variables set for the helper function we created we are going to call that for each association type:

    // Create quote template association
    const templateAssociation = createAssociations([QuoteTemplateID], "HUBSPOT_DEFINED", QuoteToTemplateAssociationTypeID);
   
    // Check if there are values in AssociatedLineItems and create associations
    const dealAssociations = createAssociations([hs_object_id], "HUBSPOT_DEFINED", QuoteToDealsAssociationTypeID);
  
    // Check if there are values in AssociatedLineItems and create associations
    const lineItemAssociations = associatedLineItems.length > 0
    ? createAssociations(associatedLineItems, "HUBSPOT_DEFINED", QuoteToLineitemsAssociationTypeID)
    : [];

  // Check if there are values in AssociatedContacts and create associations
  const contactAssociations = associatedContacts.length > 0
    ? createAssociations(associatedContacts, "HUBSPOT_DEFINED", QuoteToContactsAssociationTypeID)
    : [];

  // Check if there are values in AssociatedCompanies and create associations
  const companyAssociations = associatedCompanies.length > 0
    ? createAssociations([associatedCompanies[0]], "HUBSPOT_DEFINED", QuoteToCompaniesAssociationTypeID)
    : [];

Note that quotes must have a template. The ID for that was passed at the top of the code.

This workflow will be triggered by a deal entering a certain deal stage. So we will have a deal association and do not need to perform checks.

For the line items, contacts and companies we may have none, one or multiple associations. Because of that we will do a simple check for the length and pass an empty array if the length is nul. 

Setting Properties for the Quote

Next, the script sets the properties for the quote. These properties include the quote's title, expiration date, owner, template, and sender details:

  const properties = {
    // required properties
    "hs_title": "testQuote - " + dealname,
    "hs_expiration_date": renew_date,
    "hubspot_owner_id": hubspot_owner_id,
    "hs_template": null,
    "hs_template_type": "CUSTOMIZABLE_QUOTE_TEMPLATE",
    "hs_currency": deal_currency_code,
    // additional properties
    "hs_sender_image_url": null,
    "hs_sender_company_name": "Reus",
    "hs_sender_firstname": "Willem",
    "hs_sender_company_domain": null,
    "hs_sender_lastname": "Reus",
    "hs_sender_company_address": null,
    "hs_sender_email": "wreus@hubspot.com",
    "hs_sender_company_address2": null,
    "hs_sender_phone": null,
    "hs_sender_company_city": null,
    "hs_sender_jobtitle": null,
    "hs_sender_company_state": null,
    "hs_sender_company_zip": null,
    "hs_sender_company_country": "Ireland",
    "hs_sender_company_image_url": "https://api-na1.hubapi.com/avatars/v1/signed-uris/1CkIKDQgEEgl3cmV1cy5jb20Y9NTYiAYgZCoaYnJhbmRpbmc6YXBpOndlYjp1cy1lYXN0LTEyDTE3Mi4xNi40NS4yMTASGQB7DcdkhJJNouKsh9cpPJozE-YvoD3Z_LI"  
  };

Making the call

And this is where the magic happens. With associations and properties defined, we proceed to the creation of the quote. The HubSpot SDK simplifies this step, allowing for a direct method call to create the quote with the specified properties and associations.

In the HubSpot Quotes API documentation. there are more complex examples, but right now that is not required, making the call quite simple:

  try {
      const response = await hubspotClient.crm.quotes.basicApi.create({
          properties: {
            ...properties
          },
          associations: [
            ...templateAssociation,
            ...dealAssociations,
            ...lineItemAssociations,
            ...contactAssociations,
            ...companyAssociations,
          ]
      });

      // Log the newly created quote ID to the console for troubleshooting purposes.
      console.log(`Successfully created quote ${response.id}`);
      const quoteID = response.id;

 

In the past bit we will return only the ID of the new quote as that is made in a pre-draft state. HubSpot does not allow you to create the quote and put it into draft in the same call. This is. because otherwise the associations can not be checked and quotes without for instance a template associated to them will cause errors. So straight after the successful call we will make a second call. That call is fully supported by the SDK, so we will use that:

    // Patch quote to make this editable.    
    await hubspotClient.crm.quotes.basicApi.update(quoteID, {
      properties: {
        "hs_status": "DRAFT",
      }
    }); 

Wrapping up the code

Now that we have completed the main functionality we will just need to wrap up. First let's pass the quote ID back, so we can make that available in the rest of the workflow:

    callback({ 
      outputFields: {
          quoteID: quoteID
      }
    }); // end callback  

To ensure that any errors during the execution are properly handled and do not cause a complete halt to the application, the code includes a try/catch block:

  } catch (e) {
    e.message === 'HTTP request failed'
      ? console.error(JSON.stringify(e.response, null, 2))
      : console.error(e)
  }
}

Conclusion

The code provided in this article demonstrates how to create a new quote in HubSpot using the CRM using the SDK alone and is an update version using SDK 11 and NodeJS 20.x. It illustrates how to initialize a HubSpot client, define input fields, retrieve associated items, create associations, set properties, and create and update a quote.

Remember to handle errors gracefully, as demonstrated in the try/catch block. This ensures that any issues that arise during the execution are properly dealt with, rather than causing a complete halt to your application.

This code can be modified and expanded upon to suit your specific needs and scenarios. The HubSpot API documentation is a great resource for learning more about what's possible with the HubSpot APIs, including the CRM Quotes API and SDK.

For more information on working with the HubSpot API and custom actions, please refer to the articles on Kickbox Verification and Cloning Deals and Cloning Line Items.

Happy coding!

The complete code can be found below:

// Quote template we will use.
// A list of available templates can be retrieved by GET /crm/v4/objects/quote_template?properties=hs_name,hs_active
// See also https://developers.hubspot.com/docs/api/crm/quotes
const QuoteTemplateID = 157057354080;

// Set associationTypeID's: https://developers.hubspot.com/docs/api/crm/associations#:~:text=Retrieve%20association%20types
const QuoteToTemplateAssociationTypeID = 286
const QuoteToDealsAssociationTypeID = 64; 
const QuoteToLineitemsAssociationTypeID = 67;  
const QuoteToContactsAssociationTypeID = 69;   
const QuoteToCompaniesAssociationTypeID = 71;  

//Import required libraries
const hubspot = require('@hubspot/api-client'); // using HubSpot's SDK for most API calls

exports.main = async (event, callback) => { 
    //Create a new HubSpot API Client
    const hubspotClient = new hubspot.Client({
		accessToken: process.env.PrivateAppDealActions
    });

    //Retrieving the included inputfields
    const hs_object_id = event.inputFields['hs_object_id'];
    const dealname = event.inputFields['dealname']; 
    const hubspot_owner_id = event.inputFields['hubspot_owner_id']; 
    const deal_currency_code = event.inputFields['deal_currency_code']; 
    const renew_date = event.inputFields['renew_date']; 

    // Helper function to create associations
    function createAssociations(associatedItems, associationCategory, associationTypeId) {
      return associatedItems.map(item => ({
        to: { id: item },
        types: [
          {
            associationCategory,
            associationTypeId,
          },
        ],
      }));
    }
  
  	// Helper function to fetch associations
    async function fetchAssociations(fromObjectType, fromObjectId, toObjectType) {
        try {
            const response = await hubspotClient.crm.associations.v4.basicApi.getPage(fromObjectType, fromObjectId, toObjectType, undefined, 100);
            return response.results.map(({ toObjectId }) => String(toObjectId));
        } catch (error) {
            console.error(`Failed to fetch ${toObjectType} associations for ${fromObjectType} ${fromObjectId}:`, error.message);
            return [];
        }
    }

    // Run fetchAssociations calls in parallel for efficiency
    const [associatedLineItems, associatedContacts, associatedCompanies] = await Promise.all([
        fetchAssociations('deals', hs_object_id, 'line_items'),
        fetchAssociations('deals', hs_object_id, 'contacts'),
        fetchAssociations('deals', hs_object_id, 'companies'),
    ]);

	// For troubleshooting:
     console.log("Associated Line Items:", associatedLineItems);
    // console.log("Associated Contacts:", associatedContacts);
    // console.log("Associated Companies:", associatedCompanies);
  
  
    // Create quote template association
    const templateAssociation = createAssociations([QuoteTemplateID], "HUBSPOT_DEFINED", QuoteToTemplateAssociationTypeID);
   
    // Check if there are values in AssociatedLineItems and create associations
    const dealAssociations = createAssociations([hs_object_id], "HUBSPOT_DEFINED", QuoteToDealsAssociationTypeID);
  
    // Check if there are values in AssociatedLineItems and create associations
    const lineItemAssociations = associatedLineItems.length > 0
    ? createAssociations(associatedLineItems, "HUBSPOT_DEFINED", QuoteToLineitemsAssociationTypeID)
    : [];

  // Check if there are values in AssociatedContacts and create associations
  const contactAssociations = associatedContacts.length > 0
    ? createAssociations(associatedContacts, "HUBSPOT_DEFINED", QuoteToContactsAssociationTypeID)
    : [];

  // Check if there are values in AssociatedCompanies and create associations
  const companyAssociations = associatedCompanies.length > 0
    ? createAssociations([associatedCompanies[0]], "HUBSPOT_DEFINED", QuoteToCompaniesAssociationTypeID)
    : [];
  
   
  // Setting properties for the quote
  const properties = {
    // Setting minimum properties
    "hs_title": "testQuote - " + dealname,
    "hs_expiration_date": renew_date,
    "hubspot_owner_id": hubspot_owner_id,
    // Add all properties you believe are helpful, like:
    "hs_template": null,
    "hs_template_type": "CUSTOMIZABLE_QUOTE_TEMPLATE",
    "hs_currency": deal_currency_code,
    "hs_sender_image_url": null,
    "hs_sender_company_name": "Reus",
    "hs_sender_firstname": "Willem",
    "hs_sender_company_domain": null,
    "hs_sender_lastname": "Reus",
    "hs_sender_company_address": null,
    "hs_sender_email": "wreus@hubspot.com",
    "hs_sender_company_address2": null,
    "hs_sender_phone": null,
    "hs_sender_company_city": null,
    "hs_sender_jobtitle": null,
    "hs_sender_company_state": null,
    "hs_sender_company_zip": null,
    "hs_sender_company_country": "Ireland",
    "hs_sender_company_image_url": "https://api-na1.hubapi.com/avatars/v1/signed-uris/1CkIKDQgEEgl3cmV1cy5jb20Y9NTYiAYgZCoaYnJhbmRpbmc6YXBpOndlYjp1cy1lYXN0LTEyDTE3Mi4xNi40NS4yMTASGQB7DcdkhJJNouKsh9cpPJozE-YvoD3Z_LI"  
  };
  
  try {
      const response = await hubspotClient.crm.quotes.basicApi.create({
          properties: {
            ...properties
          },
          associations: [
            ...templateAssociation,
            ...dealAssociations,
            ...lineItemAssociations,
            ...contactAssociations,
            ...companyAssociations,
          ]
      });

      // Log the newly created quote ID to the console for troubleshooting purposes.
      console.log(`Successfully created quote ${response.id}`);
      const quoteID = response.id;

      // Patch quote to make this editable
      await hubspotClient.crm.quotes.basicApi.update(quoteID, {
          properties: {
              "hs_status": "DRAFT"
          }
      });

      // Pass information back as a data output to use as a data input with Copy to Property workflow action
      callback({
          outputFields: {
              quoteID: quoteID
          }
      });

  } catch (e) {
    e.message === 'HTTP request failed'
      ? console.error(JSON.stringify(e.response, null, 2))
      : console.error(e)
  }
}