Diving into the New HubSpot API v3 with Real-World Use Cases

Introduction

A few weeks ago, HubSpot pushed the v3 version of its API from Beta to Developer Preview, introducing some new game-changing search and import functionalities for developers. To help you better understand these new APIs, we’ll dive deep into the new features and walk through a couple of real-world use cases with code examples. Let’s get started!

Search API

The new Search API allows developers to filter, sort, and search their HubSpot CRM across any object type, including contacts, companies, deals, tickets, products, line items, and quotes (engagements are currently not supported). Default and custom HubSpot properties on any of these objects can be used to search for and return specific records that match your custom filter criteria. Let’s dive a little deeper into the capabilities of these new features and how you can get started with using them:

Filter

With the filter endpoint, you can programmatically return CRM object records that match a given field criterion, the same way you can filter records via HubSpot’s front-end UI. You can combine multiple filters together to utilize AND/OR logic to broaden or narrow your results and can even find associations for Deals and Tickets.

Let’s say you want to return all “hot” deals in your instance, which are determined to be deals with (an amount over $10,000 AND have the value of “standard” for a custom property called “Deal Type”) OR have Deal Type equal to “hot”. Here’s how this API request would look in Python:


url = f'https://api.hubapi.com/crm/v3/objects/deals/search?hapikey={hapikey}'
headers = {"Content-Type": "application/json"}
payload = {
    "filterGroups": [
        {
            "filters": [
                {
                    "propertyName": "amount",
                    "operator": "GTE",
                    "value": 10000
                },
                {
                    "propertyName": "deal_type",
                    "operator": "EQ",
                    "value": "standard"
                }
            ]
        },
        {
            "filters": [
                {
                    "propertyName": "deal_type",
                    "operator": "EQ",
                    "value": "hot"
                }
            ]
        }
    ]
}

As you can see, multiple filters within a filterGroup will be combined using the AND operator, while multiple filterGroups are combined using the OR operator. You can also declare the operator type for each filter within a filterGroup. In our example, we used GTE to declare greater than or equal to and EQ to declare equal to. Check out the official API docs for the full list of available filter operators.

Sort

The sort endpoint allows you to return your results in a specific ascending or descending order. This can be used in conjunction with the filter endpoint to allow you to view your returned results in the desired order, which can be especially useful for data analysis or review.

Continuing our previous example, if we wanted to return our results sorted by amount in descending order, our payload would need to be updated with a sorts parameter:


payload = {
    "filterGroups": [
        {
            "filters": [
                {
                    "propertyName": "amount",
                    "operator": "GTE",
                    "value": 10000
                },
                {
                    "propertyName": "deal_type",
                    "operator": "EQ",
                    "value": "standard"
                }
            ]
        },
        {
            "filters": [
                {
                    "propertyName": "deal_type",
                    "operator": "EQ",
                    "value": "hot"
                }
            ]
        }
    ]
    "sorts": [
        {
            "propertyName": "amount",
            "direction": "DESCENDING"
        }
    ]
}

Search

Lastly, you can now text search against all records across a given object. For example, you can search all Companies with a property that has a value containing the string “tech”. This means that if “tech” is found in any Company property for a given Company record, that record will be returned. Simply specify the search string as the value for the query key in your payload:


payload = {
    "query": "tech"
}

One thing to note here is that the search functionality is currently limited to certain properties.

Imports API

This revamped API allows developers to import data into HubSpot via a file without having to log in and use the frontend import tool. Instead, you can import your CSV or Excel files directly through the API, allowing you to create apps that import files programmatically. You can also specify cross-object associations (as you can via the UI), which you declare in the payload. Our simple example CSV has three columns: Deal Name, Deal Type, and Company ID; that last column associates a company record with the deal record. Here is what this payload would look like:


{
  "name": "deals_import_20200501",
  "files": [
    {
      "fileName": "deals_import_20200501.csv",
      "fileImportPage": {
        "hasHeader": true,
        "columnMappings": [
          {
            "ignored": false,
            "columnName": "Deal Name",
            "idColumnType": null,
            "propertyName": "name",
            "foreignKeyType": null,
            "columnObjectType": "DEAL",
            "associationIdentifierColumn": false
          },
          {
            "ignored": false,
            "columnName": "Deal Type",
            "idColumnType": null,
            "propertyName": "deal_type",
            "foreignKeyType": null,
            "columnObjectType": "DEAL",
            "associationIdentifierColumn": false
          },
          {
            "ignored": false,
            "columnName": "Company ID",
            "idColumnType": "HUBSPOT_OBJECT_ID",
            "propertyName": null,
            "foreignKeyType": {
              "associationCategory": "HUBSPOT_DEFINED",
              "associationTypeId": 5
            },
            "columnObjectType": "COMPANY",
            "associationIdentifierColumn": false
          }
        ]
      }
    }
  ]
}

Real-World Use Cases

Now that we have a basic understanding of the capabilities that the new Search and Import APIs offer, let’s take a look at some real-world examples to see these features in action.

Real-Time Integration

Let’s say you’re developing a custom one-way integration between HubSpot and your ERP system, which manages order, invoice, and other accounting information. When an Account record gets created in the ERP, you need a corresponding Company record to be created in HubSpot for sales and marketing outreach. Similarly, when an existing ERP Account record gets updated with new information, its corresponding HubSpot Company record needs to be updated to reflect the new changes. So in order for this logic to work, your integration app needs to be able to know when to create a new company record and when to update an existing company record. 

Prior to the v3 release, the HubSpot APIs could only update a company record (via a PUT request) if it was given the unique HubSpot company ID. So a typical approach might leverage an intermediary database layer that holds lookup tables to enable your integration app to check if a record already exists or not. Specifically, the ERP’s internal Account ID would map to a HubSpot Company ID so your app could find and update the correct company. This approach works just fine, but it introduces another layer to have to develop, manage, and bug-check and can potentially incur more managed costs.

Let’s see how the filter API can be leveraged to eliminate the need for a lookup database altogether, saving development time and tech overhead:

The first step here is to determine which field is being used to look up companies. As you probably already know, it’s never a great idea to rely on strict text matching, as you’re prone to find false positives caused by text variation. So we won’t want to match the ERP Account’s name to the HubSpot Company’s name, as an extra space, comma, dash, or any other string could prevent matches.

Instead, we’ll want to use some unique ID field that can’t be altered. In this case, it makes sense to leverage the ERP Account ID. When a new HubSpot company is created, the ERP Account ID should be written to a custom HubSpot field called “ERP Account ID”. Then, when an account gets updated in the ERP, the integration app can lookup the ERP Account ID in HubSpot companies. If it’s found, the app will know to update an existing company; otherwise, a new company should be created with the value of the ERP Account ID.

Let’s see what this logic would look like in a Python script. First, let’s build our filter function to return the HubSpot company that contains a given ERP Account ID:


import requests

def search_company_by_account_id(erp_account_id):
    url = f'https://api.hubapi.com/crm/v3/objects/companies/search?hapikey={hapikey}'
    headers = {"Content-Type": "application/json"}
    payload = {
        "filterGroups": [
            {
                "filters": [
                    {
                        "propertyName": "erp_account_id",
                        "operator": "EQ",
                        "value": erp_account_id
                    }
                ]
            }
        ],
        "properties": [
            "erp_account_id"
        ]
    }
    res = requests.post(url, headers=headers, data=json.dumps(payload))
    print(res.text)

    return res.json()

This simple function takes in an erp_account_id value that it uses to filter companies and return the value of the HubSpot Company ID and erp_account_id. We’re using the requests package to post our payload and are simply printing the text response and returning the JSON response (containing the erp_account_id) to use later in our script.

We’ll also need functions to create a HubSpot company and update a HubSpot company, which will be called in the logic portion of our script. Here’s what these functions might look like:


def create_hs_company(data):
    url = f"https://api.hubapi.com/companies/v2/companies?hapikey={hapikey}"
    headers = {"Content-Type": "application/json"}
    data = {
        "properties": [
            {
                "name": "name",
                "value": data['company_name']
            },
            {
                "name": "erp_account_id",
                "value": data['erp_account_id']
            },
            {
                "name": "phone",
                "value": data['phone_number']
            }
        ]
    }
    res = requests.post(url, headers=headers, data=json.dumps(data))
    print(res.text)


def update_hs_company(data, company_id):
    url = f'https://api.hubapi.com/companies/v2/companies/{company_id}?hapikey={hapikey}'
    headers = {"Content-Type": "application/json"}
    data = {
        "properties": [
            {
                "name": "name",
                "value": data['company_name']
            },
            {
                "name": "erp_account_id",
                "value": data['erp_account_id']
            },
            {
                "name": "phone",
                "value": data['phone_number']
            }
        ]
    }
    res = requests.put(url, headers=headers, data=json.dumps(data))
    print(res.text)

These functions are nearly identical in that they take in JSON objects containing company_name, erp_account_id, and phone_number and post the payloads with requests. The update_hs_company function also takes in the HubSpot company ID so it knows which company record to update.

Now, we can call these helper functions in our main workhorse function that handles the business logic. First, we’ll call the search_company_by_account_id function to try to find a HubSpot company whose ERP Account ID field matches the account ID number we’re passing into it. If a record is found, we’ll call the update_hs_company function to update the record with any new data. If a record isn’t found, a new company record needs to be created, so we’ll call the create_hs_company function.

Here’s what this logic might look like:


# search HubSpot Company by ERP account ID
search_results = search_company_by_account_id(company_data['erp_account_id'])
# if a company is found, define HubSpot company ID and update company record
if search_results['results']:
    # search_results can contain multiple records; only expecting one
    company_id = search_results['results'][0]['id']  
    update_hs_company(company_data, company_id)
# else, company is not found; create new HubSpot company
else:
    create_hs_company(company_data)

Here, we’re calling the search_company_by_account_id function with erp_account_id passed into it and assigning the JSON response to a variable called search_results. If there are results in that response, we’re indexing into the results object, pulling out the HubSpot company ID, and updating that company with the new data we’ve received. If there is no results object in the search response, this means a company does not exist with the account_id, so we’re calling the create_hs_company function to (you guessed it) create a new company.

Note that this logic block assumes we’ve assigned our newly pulled data to a dictionary called company_data, which contains erp_account_id, company_name, and phone_number.

You can apply this logic regardless of whether your app receives data via a webhook or queries data from a flat file or the ERP directly. And with this logic that uses the Search API, we don’t need to write, update, and look up IDs in a lookup database, saving development time and overhead.

SFTP Server

The Imports API could be a great option to help automate an integration that leverages an SFTP server sitting between HubSpot and a third-party system. If you have clean and ready-to-import CSVs dumped to your SFTP server each day, the Import API endpoint could be used to simply upload that CSV directly into HubSpot.

Let’s say you have a custom Django instance that stores and manages customer subscription statuses for a software product your company sells. The Django instance is built to run a nightly job to query the internal database for subscription updates and dump the results to a CSV on an SFTP server. We’ve written the database query to return a contact’s first name, last name, email, and product subscription status and have formatted the columns to be friendly with the HubSpot Contact data model. Our CSV might look like this:


first_name,last_name,email,prod_sub_status
Michael,Jordan,jumpman23@jordan.com,expired
LeBron,James,lbj23@lakers.com,active
Steph,Curry,chefcurry30@warriors.com,trial

As long as the columns are static and won’t change, we can write our script to post this file to HubSpot using the requests package. Here’s what our function might look like:


def import_file():
    post_url = f'https://api.hubapi.com/crm/v3/imports/?hapikey={hs_api_key}'
    payload = {
      "name": "my_import",
      "files": [
        {
          "fileName": "file.csv",
          "fileImportPage": {
            "hasHeader": True,
            "columnMappings": [
                {
                    "ignored": False,
                    "columnName": "first_name",
                    "propertyName": "firstname",
                    "columnObjectType": "CONTACT",
                    "associationIdentifierColumn": False
                },
                {
                    "ignored": False,
                    "columnName": "last_name",
                    "propertyName": "lastname",
                    "columnObjectType": "CONTACT",
                    "associationIdentifierColumn": False
                },
                {
                    "ignored": False,
                    "columnName": "email",
                    "idColumnType": "HUBSPOT_ALTERNATE_ID",
                    "propertyName": "email",
                    "columnObjectType": "CONTACT",
                    "associationIdentifierColumn": False
                },
                {
                    "ignored": False,
                    "columnName": "status",
                    "propertyName": "product_subscription_status",
                    "columnObjectType": "CONTACT",
                    "associationIdentifierColumn": False
                }
            ]
          }
        }
      ]
    }
    files = {
        'importRequest': (None, json.dumps(payload), 'application/json'),
        'files': open('your/file/path/file.csv', 'rb')
    }
    res = requests.post(post_url, files=files)
    print(res)
    print(res.text)

Conclusion

The new search and import capabilities included in HubSpot’s new v3 APIs are game changers for developers. The search functionality can, in many cases, eliminate the need for an intermediary lookup database when pushing new or updated data from a third-party software into HubSpot, saving development time and infrastructure upkeep. Troubleshooting, data review, and backend reports can be quickly and easily performed with the ability to easily pull selected data from your HubSpot portal. And the new import functionality means you can programmatically import your CSVs and Excel spreadsheets into HubSpot without having to log into HubSpot’s frontend and use the manual import wizard.

We’ve already begun implementing these new features in migration and integration projects for our clients and have seen great results so far. Be sure to check out our blog for more tech-focused articles like this.