GuidesQuickstart guidesBuild a HubSpot Deal Data API with Python and Flask

Build a HubSpot Deal Data API with Python and Flask

A guide by

Introduction #

HubSpot is one of the leading CRMs used by companies of all sizes. HubSpot's API is great for pulling data for deals, contacts, and companies in your CRM. These associates can be visible within HubSpot but sometimes, you need to interact with this data programmatically with the associations merged in a clean, readable format.

This guide and the provided templates will show you how to fetch deals from HubSpot along with their associated contacts, companies, and owners, while respecting HubSpot's API rate limits. Once you get this associated data, you’ll be able to deploy this custom API as your own API for your team to use.

Using our templates, you can have your internal HubSpot Deals API up and running in 5 minutes or less.

Getting started #

This guide comes with two sections:

  1. Creating the simple data aggregation script. Full code for this project here.
  2. Converting this script into an internal API. Full code for this project here.

For either template, click "Use template" and name your project however you like.

Create a Private app in HubSpot

Log in to your HubSpot account and navigate to Settings > Integrations > Private Apps

HubSpot Private apps creation UI - 2024-06-28

Click "Create a private app” and set a name for your app.

HubSpot Private app naming UI - 2024-06-24

Set the necessary scopes (you'll need access to deals, contacts, companies, and owners).

HubSpot Private app scopes UI

Once your scopes are set, click Create app. You'll receive a private app access token.

To keep this secret safe, add it to the Secrets tab in your Repl. In the bottom-left corner of the Replit workspace, there is a section called "Tools."

Select "Secrets" within the Tools pane, and add your Private App access token to the Secret labeled HUBSPOT_ACCESS_TOKEN.

Replit Secrets UI adding a HUBSPOT_ACCESS_TOKEN

Setting up the HubSpot client and rate limiting #

Let's start by setting up our HubSpot client and implementing a rate limiting system that keeps our API calls within HubSpots rate limit of 10 calls per second:

python

1 2 3 4 5 6 7 8 9 10 11 12 import json import os import simplejson from hubspot import HubSpot from hubspot.crm.deals import ApiException from urllib3.util.retry import Retry # Initialize the HubSpot client with retry logic hubspot_access_token = os.environ['HUBSPOT_ACCESS_TOKEN'] retry = Retry(total=5, status_forcelist=(429,)) hubspot_client = HubSpot(access_token=hubspot_access_token, retry=retry)

In Replit, os.environ['HUBSPOT_ACCESS_TOKEN'] will automatically fetch the token we stored in the Secrets tab.

Fetching deals #

Now let's create a function to fetch deals:

python

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 def fetch_deals_with_associations(): try: # Fetch 25 deals deals_page = hubspot_client.crm.deals.basic_api.get_page(limit=25) deals = deals_page.results # Process each deal deals_with_associations = [process_deal(deal) for deal in deals] # Log the response to the console print("Deals with associations have been fetched successfully") print(simplejson.dumps(deals_with_associations, indent=2, ignore_nan=True, default=str)) return deals_with_associations except ApiException as e: print(f"Exception when fetching deals: {e}\n") return f"Error: {e}\n"

This function fetches 25 deals, processes each one, and then prints the results using simplejson for better handling of complex data types. If you would like to pull more deals from your HubSpot CRM, simply increase the limit parameter in this line:

deals_page = hubspot_client.crm.deals.basic_api.get_page(limit=25)

Processing deals and fetching associated data #

Let's create a function to process each deal and fetch its associated data:

python

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 def process_deal(deal): deal_dict = deal.to_dict() try: # Get associated contacts contacts_page = hubspot_client.crm.associations.v4.basic_api.get_page( 'deals', deal.id, 'contacts') contact_ids = [result.to_object_id for result in contacts_page.results] # Get associated companies companies_page = hubspot_client.crm.associations.v4.basic_api.get_page( 'deals', deal.id, 'companies') company_ids = [result.to_object_id for result in companies_page.results] # Fetch owner details owner_email = None if deal.properties.get('hubspot_owner_id'): try: owner = hubspot_client.crm.owners.owners_api.get_by_id( owner_id=deal.properties['hubspot_owner_id'], id_property='id', archived=False) owner_email = owner.properties.get('email') except ApiException as e: print(f"Exception when fetching owner details: {e}\n") # Fetch full contact and company details contacts_batch = hubspot_client.crm.contacts.batch_api.read( batch_read_input_simple_public_object_id={ "properties": ['email', 'firstname', 'lastname'], "inputs": [{"id": id} for id in contact_ids] }) if contact_ids else None companies_batch = hubspot_client.crm.companies.batch_api.read( batch_read_input_simple_public_object_id={ "properties": ['name', 'domain', 'hs_additional_domains', 'website'], "inputs": [{"id": id} for id in company_ids] }) if company_ids else None # Format contact details formatted_contacts = [] if contacts_batch and contacts_batch.results: for contact in contacts_batch.results: contact_dict = contact.to_dict() formatted_contacts.append({ "contact_id": int(contact_dict['id']), "contact_email_addresses": [ contact_dict['properties'].get('email') ] if contact_dict['properties'].get('email') else [], "contact_name": f"{contact_dict['properties'].get('firstname', '')} {contact_dict['properties'].get('lastname', '')}".strip() }) # Format company details formatted_companies = [] if companies_batch and companies_batch.results: for company in companies_batch.results: company_dict = company.to_dict() primary_domain = company_dict['properties'].get('domain') additional_domains = company_dict['properties'].get( 'hs_additional_domains', '').split(';') if company_dict['properties'].get( 'hs_additional_domains') else [] website = company_dict['properties'].get('website') all_domains = list( set(filter(None, [primary_domain, website] + additional_domains))) formatted_companies.append({ "company_id": int(company_dict['id']), "company_name": company_dict['properties'].get('name', ''), "company_domains": all_domains }) # Combine deal with its associations and owner email return { **deal_dict, "owner_email": owner_email, "associatedContacts": formatted_contacts, "associatedCompanies": formatted_companies } except ApiException as e: print(f"Exception when processing deal {deal.id}: {e}\n") return deal_dict

Tip: If you want a deeper explanation of what a certain section of code does in your project, simply highlight the lines in question, right click, and select “Explain with AI”.

Running the script #

To run our script, we add this at the end of the file:

python

1 2 # Run the function fetch_deals_with_associations()

Once that’s in, hit the Run button in your Repl, if all is set up correctly, you will see your data appear in the Console. From here you can ask Replit AI to turn the results into a CSV, txt file, or continue adding more functionality once you receive your aggregated Deals data.

If you’d like to turn this into an internal API that the rest of your team can use, read on!

Converting your script into an API #

To make your HubSpot deal data aggregator more accessible to your team, let's convert it into a Flask API. This will allow other members of your organization to request deal data programmatically.

Setting up Flask #

Let’s modify our script to convert it into a Flask application. At the bottom of main.py replace the fetch_deals_with_associations() function with the following:

python

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 app = Flask(__name__) # Wrapper function to require API key def require_api_key(f): @wraps(f) def decorated_function(*args, **kwargs): api_key = request.headers.get('Authorization') if api_key and api_key == os.environ.get('INTERNAL_API_KEY'): return f(*args, **kwargs) else: return jsonify({"error": "Unauthorized"}), 401 return decorated_function @app.route('/get-deals-with-associations', methods=['GET']) @require_api_key def get_deals(): deals = fetch_deals_with_associations() return jsonify(deals) if __name__ == '__main__': app.run(host='0.0.0.0', port=5000)

Add these imports at the top:

python

1 2 from flask import Flask, jsonify, request from functools import wraps

Your final main.py file should be:

python

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 import os from functools import wraps import simplejson from flask import Flask, jsonify, request from hubspot import HubSpot from hubspot.crm.deals import ApiException from urllib3.util.retry import Retry # Initialize the HubSpot client with retry logic hubspot_access_token = os.environ['HUBSPOT_ACCESS_TOKEN'] retry = Retry(total=5, status_forcelist=(429, )) hubspot_client = HubSpot(access_token=hubspot_access_token, retry=retry) def fetch_deals_with_associations(): try: # Fetch 25 deals deals_page = hubspot_client.crm.deals.basic_api.get_page(limit=25) deals = deals_page.results # Process each deal deals_with_associations = [process_deal(deal) for deal in deals] # Log the response to the console print("Deals with associations have been fetched successfully") print(simplejson.dumps(deals_with_associations, indent=2, ignore_nan=True, default=str)) return deals_with_associations except ApiException as e: print(f"Exception when fetching deals: {e}\n") return f"Error: {e}\n" def process_deal(deal): deal_dict = deal.to_dict() try: # Get associated contacts contacts_page = hubspot_client.crm.associations.v4.basic_api.get_page( 'deals', deal.id, 'contacts') contact_ids = [result.to_object_id for result in contacts_page.results] # Get associated companies companies_page = hubspot_client.crm.associations.v4.basic_api.get_page( 'deals', deal.id, 'companies') company_ids = [result.to_object_id for result in companies_page.results] # Fetch owner details owner_email = None if deal.properties.get('hubspot_owner_id'): try: owner = hubspot_client.crm.owners.owners_api.get_by_id( owner_id=deal.properties['hubspot_owner_id'], id_property='id', archived=False) owner_email = owner.properties.get('email') except ApiException as e: print(f"Exception when fetching owner details: {e}\n") # Fetch full contact and company details contacts_batch = hubspot_client.crm.contacts.batch_api.read( batch_read_input_simple_public_object_id={ "properties": ['email', 'firstname', 'lastname'], "inputs": [ { "id": id } for id in contact_ids ] }) if contact_ids else None companies_batch = hubspot_client.crm.companies.batch_api.read( batch_read_input_simple_public_object_id={ "properties": ['name', 'domain', 'hs_additional_domains', 'website'], "inputs": [ { "id": id } for id in company_ids ] }) if company_ids else None # Format contact details formatted_contacts = [] if contacts_batch and contacts_batch.results: for contact in contacts_batch.results: contact_dict = contact.to_dict() formatted_contacts.append({ "contact_id": int(contact_dict['id']), "contact_email_addresses": [ contact_dict['properties'].get('email') ] if contact_dict['properties'].get('email') else [], "contact_name": f"{contact_dict['properties'].get('firstname', '')} {contact_dict['properties'].get('lastname', '')}".strip() }) # Format company details formatted_companies = [] if companies_batch and companies_batch.results: for company in companies_batch.results: company_dict = company.to_dict() primary_domain = company_dict['properties'].get('domain') additional_domains = company_dict['properties'].get( 'hs_additional_domains', '').split(';') if company_dict['properties'].get( 'hs_additional_domains') else [] website = company_dict['properties'].get('website') all_domains = list( set(filter(None, [primary_domain, website] + additional_domains))) formatted_companies.append({ "company_id": int(company_dict['id']), "company_name": company_dict['properties'].get('name', ''), "company_domains": all_domains }) # Combine deal with its associations and owner email return { **deal_dict, "owner_email": owner_email, "associatedContacts": formatted_contacts, "associatedCompanies": formatted_companies } except ApiException as e: print(f"Exception when processing deal {deal.id}: {e}\n") return deal_dict # Run the flask app app = Flask(__name__) # Wrapper function to require API key def require_api_key(f): @wraps(f) def decorated_function(*args, **kwargs): api_key = request.headers.get('Authorization') if api_key and api_key == os.environ.get('INTERNAL_API_KEY'): return f(*args, **kwargs) else: return jsonify({"error": "Unauthorized"}), 401 return decorated_function @app.route('/get-deals-with-associations', methods=['GET']) @require_api_key def get_deals(): deals = fetch_deals_with_associations() return jsonify(deals) if __name__ == '__main__': app.run(host='0.0.0.0', port=5000)

Hit the Run button again. If everything is set up correctly, you should get the Webview tab to appear like this:

Replit WebView with title "Not Found"

Securing your API #

Next, we want to be sure that only members of our team will be able to send requests to our new API. Here’s how we can set up an internal key.

First, head to your Secrets tab again and add a new key called INTERNAL_API_KEY. For your value, add any password or secret string you like. Ideally, you should generate a 15-20 character string with a mix of numbers, letters, and symbols. Paste this in as your value.

You can store this value in a secure password manager like 1Password to share with the rest of your team.

Testing your API #

Once your Flask app is running, it will automatically have its own development environment URL. To find it, click on the Webview URL preview it should show a pop up like this:

Replit development environment url pop up

You can also find your development URL by typing ⌘ + K (or Ctrl + K) and typing "networking". Copy this URL to your clipboard.

Replit networking tab

Then, open the Shell and enter the following command. Replace YOUR_INTERNAL_API_KEY with the value in your Secrets and YOUR_DEV_URL with the URL copied from the Webview or Networking tab.

python

1 curl -H "Authorization: YOUR_INTERNAL_API_KEY" YOUR_DEV_URL/get-deals-with-associations

Deploying your API #

Open a new tab in the Workspace and search for “Deployments” or open the control console by typing ⌘ + K (or Ctrl + K) and type "deploy". You should find a screen like this.

Replit Deployments selection UI

For APIs like this, we recommend using Autoscale. Click on Set up your deployment to continue. On the next screen, click Approve and configure build settings most internal APIs work fine with the default machine settings but if you need more power later, you can always come back and change these settings later. You can monitor your usage and billing at any time at: replit.com/usage.

On the next screen, you’ll be able to set your primary domain and edit the Secrets that will be in your production deployment. Usually, we keep these settings as they are.

Finally, click Deploy and watch your API go live!

If your deployment succeeds, you now have a production URL and /get-deals-with-associations endpoint for the rest of your team to pull clean, readable data directly from your HubSpot CRM.

Conclusion #

You've now created a Flask API in Replit that fetches deals from HubSpot along with their associated contacts, companies, and owners. You can now integrate this API and data into other internal tools (e.g. Salesforce or Google Sheets).

This API respects HubSpot's rate limit of 10 requests per second limits, provides a comprehensive view of your deal data, and offers a secure way for your team to access this data programmatically.