Custom integration of HubSpot CRM and Django

None

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:

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:

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:

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, 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, for the convenience of filling the array with updated fields, we created the add_property helper function:

api.tasks.hubspot.process_webhook
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!