Route HubSpot leads by territory and company size using code

medium complexityCost: $0

Prerequisites

Prerequisites
  • Python 3.9+ or Node.js 18+
  • HubSpot private app token with crm.objects.contacts.read, crm.objects.contacts.write, and crm.objects.companies.read scopes
  • Slack Bot Token (xoxb-...) with chat:write scope
  • A server or serverless function to receive webhooks (or a cron scheduler for polling)

Step 1: Define the territory config

The territory config is the heart of this recipe. Keep it as a separate data structure so sales ops can update it without touching routing logic.

TERRITORY_CONFIG = {
    # Enterprise override (checked first)
    "enterprise_threshold": 1000,
    "enterprise_rep": {
        "owner_id": "444444", "slack_id": "U04DDDD", "name": "Dave (Enterprise)"
    },
 
    # Territory map: state abbreviation -> rep
    "territories": {
        # Northeast
        "NY": {"owner_id": "111111", "slack_id": "U01AAAA", "name": "Alice"},
        "MA": {"owner_id": "111111", "slack_id": "U01AAAA", "name": "Alice"},
        "CT": {"owner_id": "111111", "slack_id": "U01AAAA", "name": "Alice"},
        "NJ": {"owner_id": "111111", "slack_id": "U01AAAA", "name": "Alice"},
        "PA": {"owner_id": "111111", "slack_id": "U01AAAA", "name": "Alice"},
        # West Coast
        "CA": {"owner_id": "222222", "slack_id": "U02BBBB", "name": "Bob"},
        "WA": {"owner_id": "222222", "slack_id": "U02BBBB", "name": "Bob"},
        "OR": {"owner_id": "222222", "slack_id": "U02BBBB", "name": "Bob"},
        # South
        "FL": {"owner_id": "333333", "slack_id": "U03CCCC", "name": "Carol"},
        "GA": {"owner_id": "333333", "slack_id": "U03CCCC", "name": "Carol"},
        "TX": {"owner_id": "333333", "slack_id": "U03CCCC", "name": "Carol"},
    },
 
    # Default fallback
    "default_rep": {
        "owner_id": "555555", "slack_id": "U05EEEE", "name": "Eve (Catch-all)"
    },
}

Step 2: Build the routing function

import requests
import os
 
HUBSPOT_TOKEN = os.environ["HUBSPOT_TOKEN"]
HEADERS = {"Authorization": f"Bearer {HUBSPOT_TOKEN}", "Content-Type": "application/json"}
 
def get_company_for_contact(contact_id):
    """Fetch the primary associated company for a contact."""
    resp = requests.get(
        f"https://api.hubapi.com/crm/v3/objects/contacts/{contact_id}/associations/companies",
        headers=HEADERS
    )
    resp.raise_for_status()
    results = resp.json().get("results", [])
    if not results:
        return None
 
    company_id = results[0]["id"]
    resp = requests.get(
        f"https://api.hubapi.com/crm/v3/objects/companies/{company_id}",
        headers=HEADERS,
        params={"properties": "name,numberofemployees,state,country,hubspot_owner_id"}
    )
    resp.raise_for_status()
    return resp.json()
 
def route_contact(contact, company):
    """Determine the right owner based on territory and size rules."""
    config = TERRITORY_CONFIG
 
    # Priority 1: existing account owner
    if company and company.get("properties", {}).get("hubspot_owner_id"):
        return {
            "owner_id": company["properties"]["hubspot_owner_id"],
            "reason": "Existing account owner",
        }
 
    # Priority 2: enterprise override
    employees = int((company or {}).get("properties", {}).get("numberofemployees") or 0)
    if employees >= config["enterprise_threshold"]:
        rep = config["enterprise_rep"]
        return {**rep, "reason": f"Enterprise ({employees} employees)"}
 
    # Priority 3: territory match
    state = (
        (company or {}).get("properties", {}).get("state")
        or contact.get("properties", {}).get("state")
        or ""
    ).upper()
 
    if state in config["territories"]:
        rep = config["territories"][state]
        return {**rep, "reason": f"Territory: {state}"}
 
    # Fallback
    rep = config["default_rep"]
    return {**rep, "reason": f"No match (state: {state or 'unknown'})"}

Step 3: Wire it together with a webhook handler

from flask import Flask, request, jsonify
from slack_sdk import WebClient
 
app = Flask(__name__)
slack = WebClient(token=os.environ["SLACK_BOT_TOKEN"])
PORTAL_ID = os.environ.get("HUBSPOT_PORTAL_ID", "YOUR_PORTAL_ID")
 
@app.route("/webhook", methods=["POST"])
def handle_webhook():
    for event in request.json:
        if event.get("subscriptionType") != "contact.creation":
            continue
 
        contact_id = event["objectId"]
 
        # Fetch contact
        resp = requests.get(
            f"https://api.hubapi.com/crm/v3/objects/contacts/{contact_id}",
            headers=HEADERS,
            params={"properties": "firstname,lastname,email,company,jobtitle,state,country,hubspot_owner_id"}
        )
        resp.raise_for_status()
        contact = resp.json()
 
        # Skip already-assigned
        if contact["properties"].get("hubspot_owner_id"):
            continue
 
        # Get company
        company = get_company_for_contact(contact_id)
 
        # Route
        result = route_contact(contact, company)
 
        # Assign owner
        requests.patch(
            f"https://api.hubapi.com/crm/v3/objects/contacts/{contact_id}",
            headers=HEADERS,
            json={"properties": {"hubspot_owner_id": result["owner_id"]}}
        ).raise_for_status()
 
        # Notify in Slack
        name = f"{contact['properties'].get('firstname', '')} {contact['properties'].get('lastname', '')}".strip()
        company_name = (company or {}).get("properties", {}).get("name", contact["properties"].get("company", "Unknown"))
 
        if result.get("slack_id"):
            slack.chat_postMessage(
                channel=result["slack_id"],
                text=f"New lead routed: {name}",
                blocks=[
                    {"type": "section", "text": {"type": "mrkdwn",
                        "text": f"🆕 *New Lead Routed to You*\n*{name}* at {company_name}\n📍 {result['reason']}"}},
                    {"type": "actions", "elements": [
                        {"type": "button", "text": {"type": "plain_text", "text": "View in HubSpot"},
                         "url": f"https://app.hubspot.com/contacts/{PORTAL_ID}/contact/{contact_id}"}
                    ]}
                ]
            )
 
    return jsonify({"status": "ok"}), 200

Step 4: Register the webhook

curl -X POST "https://api.hubapi.com/webhooks/v3/YOUR_APP_ID/subscriptions" \
  -H "Authorization: Bearer $HUBSPOT_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"eventType": "contact.creation", "active": true}'
Existing account lookup adds latency

Each routed contact requires 2-3 extra API calls (association lookup + company fetch). For high-volume scenarios, cache company data locally or use HubSpot's batch endpoints.

Cost

  • Hosting: Free on Vercel (serverless), ~$5/mo on Railway
  • No per-execution cost beyond hosting

Need help implementing this?

We build and optimize automation systems for mid-market businesses. Let's discuss the right approach for your team.