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, andcrm.objects.companies.readscopes - Slack Bot Token (
xoxb-...) withchat:writescope - 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"}), 200Step 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.