Contact me
Custom integration of HubSpot CRM and Django

HubSpot is a system that simplifies the work of sales and marketing teams by automating routine tasks. You can configure your pipeline for deals and describe your workflow, or what will happen at each stage of the deal. It is not always possible to get by with the workflow functionality only. This may be because you are integrating with a custom company platform, or logic connected to an external database. For such cases, you can use the convenient and well-documented Hubspot API.

Our project (let's call it Portal) runs on Python 3 (Django Rest Framework), which stores a list of services sold by our client, the logic of calculating the cost of services, and more. In this article, I will describe some of the tasks that we have tackled using WebHooks and Hubspot’s API.

The pipeline of our Deal is as follows:

  • Started
  • Information Sent
  • Approved
  • Deal won
  • Completed

When the Deal stage switches to the Approved value based on the data from Hubspot, we need to create a document of the ordered service (Order), to calculate the price of this service and to update the data in HubSpot. In this case, the first thing we have to do is to create a View and configure the router to receive and process the POST request.

api.views.hubspot
For now it just immediately returns HTTP Code 200 without CSRF verification

from django.views import View
from django.http import JsonResponse
from django.utils.decorators import method_decorator
from django.views.decorators.csrf import csrf_exempt

@method_decorator(csrf_exempt, name='dispatch')
class HubspotWebhookView(View):

   def post(self, request, *args, **kwargs):
       return JsonResponse({"success": True})


api.urls

...
urlpatterns = [
   ...
   url(r'^hubspot/webhook/$', HubspotWebhookView.as_view(), name='hubspot_webhook'),
]

We are using the versioning API and as a result, our uri will look like this: /api/v1/hubspot/deal-webhook/

As Hubspot expects to receive a response within 2 seconds, we won’t delay validating the signature and running the asynchronous Celery Tasks.

api.views.hubspot
...
from api.utils.hubspot import validate_signature
from api.tasks.hubspot import process_webhook
...

@method_decorator(csrf_exempt, name='dispatch')
class HubspotWebhookView(View):

   def post(self, request, *args, **kwargs):
       validate_signature(requests)
       process_webhook.delay(request.body)
       return JsonResponse({"success": True})

validate_signature will check the request data and if the data is not valid, an Exception rest_framework.exceptions.ValidationError will be generated. Subsequently, the attacker will receive an HTTP Code 400 Bad Request. Here we finish the view setup.

We will not dwell on signature validation (api.utils.hubspot.validate_signature), as it is described in the documentation with examples in Python. The main things to know are:

 

api.tasks.hubspot.process_webhook

import json
from django.conf import settings
from api.services.hubspot import Hubspot

def process_webhook(request_body):
   body = json.loads(request_body, encoding="utf8")

   hubspot = Hubspot(api_key=settings.HUBSPOT_API_KEY, portal_id=settings.HUBSPOT_PORTAL_ID)
   hubspot.process(body)

The process_webhook task does the following:

  • parses received json string
  • creates an instance of the custom service class Hubspot Constructor of this class. Hubspot accepts two api_key parameters (needed for requests to the Hubspot API; we got it when creating the application in Hubspot https://developers.hubspot.com/docs/faq/how-do-i-create-an-app-in-hubspot ) and portal_id (the Hubspot ID of the portal our client works with)

api.services.hubspot.Hubspot

from hubspot3.deals import DealsClient
from core.utils import create_orderfrom_hubspot, update_order_cost
from api.utils.hubspot import add_property

class Hubspot(object):
   # Subscription Types
   SUBSCRIPTION_DEAL_CHANGED = "deal.propertyChange"

   # Stages
   DEAL_STAGE_APPROVED = "Approved"

   portal_id = None
   api_key = None

   def __init__(self, api_key, portal_id):
       self.portal_id = portal_id
       self.api_key = api_key

   def process(self, events):
       for event in events:
           if not event.get("portalId") is self.portal_id:
               continue
           method_name = self._get_event_method_name(event)
           self._call_method(method_name, event)


   def _get_event_method_name(self, event):
       if not event.get("subscriptionType") is self.SUBSCRIPTION_DEAL_CHANGED:
           prop_name = event["propertyName"]
           return f"deal_{prop_name}_changed"
       return None


   def _call_method(self, method_name, event):
       method_to_call = getattr(self, method_name, None)
       if method_to_call:
           method_to_call(event)

   def deal_dealstage_changed(self, event):
       if not event.get("propertyValue") is self.DEAL_STAGE_APPROVED:
           return
       deal_id = event["objectId"]


       # Get Deal From the hubspot
       deals_client = DealsClient(api_key=self.api_key)
       deal_data = deals_client.get(deal_id)
       order = create_orderfrom_hubspot(deal_data)

       update_order_cost(order)

       # Create dict of properties
       properties = {}
       add_property(properties, 'cost', order.cost)
       add_property(properties, 'order_url', order.url)

       # update properties in the HubSpot
       deals_client.update(deal_id, properties)


The main method of our service is a process, it takes a list of events and processes them in turns.
 In event, we are interested in the following fields:

  • portalId - id of the portal from which we expect to receive updates, we process data only from the portal known to us
  • subscriptionType - type of subscription, for deal changes this value is: deal.propertyChange
  • propertyName - the name of the field that has been changed; with its help we determine what method we use to process this change
  • propertyValue - The new value of the field.

According to our logic, the method that handles the change of the Hubspot Deal Stage is deal_dealstage_changed. In compliance with our task, an Order should be created only when the dealstage changes to the Approved value; we ignore all other values.
To create an Order, we need additional fields from Hubspot, which we get through the API using the hubspot3 package (https://pypi.org/project/hubspot3/), in particular, the DealsClient class.
After creating the Order, calculating its price, we update the data in the Hubspot Deal using the previously created instance of the DealsClient class. You can see the data structure for the update here (https://developers.hubspot.com/docs/methods/deals/update_deal), for the convenience of filling the array with updated fields, we created the add_property helper function:

def add_property(properties, key, value):
   if not properties.get('properties'):
       properties.update({'properties': []})
   properties.get('properties', []).append({
       'name': key,
       'value': str(value)
   })


We will be glad if the above described case is useful to the community =) 
Hope it helps, and if you have any comments about the article, feel free to share them with us!

12/04/2020

Most of the startups try to be like a swiss knife. It means, that they want to implement as many features as possible in their product.

12/04/2020

While the covid-19 affecting a different kind of businesses, Edicasoft continue working in the usual mode, as we are working remotely since founding at 2017.

12/04/2020

Most outsourcing IT companies don't have enough qualifications to work with startups, so usually, they don't give the quality value of the product and after the development, most part of MVP is just a cr*p that needs to be rewritten by the in-house team

Contacts