Round-robin route HubSpot leads and notify reps in Slack using code
high complexityCost: $0
Prerequisites
Prerequisites
- Node.js 18+ or Python 3.9+
- HubSpot private app token with
crm.objects.contacts.read,crm.objects.contacts.write, andsettings.users.readscopes - Slack Bot Token (
xoxb-...) withchat:writescope - A server or serverless function to receive webhooks (Express, Vercel, AWS Lambda)
- A persistence layer for the round-robin counter (Redis, a JSON file, or an environment variable)
Step 1: Define the rep roster
Create a config that maps each rep's HubSpot owner ID to their Slack user ID:
REPS = [
{"name": "Alice", "hubspot_owner_id": "12345678", "slack_user_id": "U01AAAA"},
{"name": "Bob", "hubspot_owner_id": "23456789", "slack_user_id": "U02BBBB"},
{"name": "Carol", "hubspot_owner_id": "34567890", "slack_user_id": "U03CCCC"},
{"name": "Dave", "hubspot_owner_id": "45678901", "slack_user_id": "U04DDDD"},
]Step 2: Build the webhook handler with round-robin logic
from flask import Flask, request, jsonify
from slack_sdk import WebClient
import requests, os, json
app = Flask(__name__)
slack = WebClient(token=os.environ["SLACK_BOT_TOKEN"])
HUBSPOT_TOKEN = os.environ["HUBSPOT_TOKEN"]
HEADERS = {"Authorization": f"Bearer {HUBSPOT_TOKEN}", "Content-Type": "application/json"}
PORTAL_ID = os.environ.get("HUBSPOT_PORTAL_ID", "YOUR_PORTAL_ID")
COUNTER_FILE = "/tmp/round_robin_counter.json"
def get_next_rep():
"""Read counter, pick the next rep, increment and save."""
try:
with open(COUNTER_FILE) as f:
data = json.load(f)
except (FileNotFoundError, json.JSONDecodeError):
data = {"index": 0}
rep = REPS[data["index"] % len(REPS)]
data["index"] = (data["index"] + 1) % len(REPS)
with open(COUNTER_FILE, "w") as f:
json.dump(data, f)
return rep
@app.route("/webhook", methods=["POST"])
def handle_webhook():
events = request.json
for event in events:
if event.get("subscriptionType") != "contact.creation":
continue
contact_id = event["objectId"]
# Fetch contact details
resp = requests.get(
f"https://api.hubapi.com/crm/v3/objects/contacts/{contact_id}",
headers=HEADERS,
params={"properties": "firstname,lastname,email,company,jobtitle,hubspot_owner_id"}
)
resp.raise_for_status()
props = resp.json()["properties"]
# Skip if already assigned
if props.get("hubspot_owner_id"):
continue
# Assign via round-robin
rep = get_next_rep()
requests.patch(
f"https://api.hubapi.com/crm/v3/objects/contacts/{contact_id}",
headers=HEADERS,
json={"properties": {"hubspot_owner_id": rep["hubspot_owner_id"]}}
).raise_for_status()
# Slack DM
name = f"{props.get('firstname', '')} {props.get('lastname', '')}".trim() if hasattr(str, 'trim') else f"{props.get('firstname', '')} {props.get('lastname', '')}".strip()
slack.chat_postMessage(
channel=rep["slack_user_id"],
text=f"New lead assigned: {name}",
blocks=[
{"type": "section", "text": {"type": "mrkdwn",
"text": f"🆕 *New Lead Assigned to You*\n*{name}* — {props.get('jobtitle', 'No title')} at {props.get('company', 'Unknown')}\nEmail: {props.get('email', 'N/A')}"}},
{"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"}), 200Counter persistence in serverless
A JSON file on /tmp works for a single server but resets on serverless cold starts. For production, use Redis, DynamoDB, or a database row. The key requirement is atomic read-and-increment to avoid race conditions.
Step 3: Register the webhook with HubSpot
Subscribe to contact creation events:
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
}'Then set your webhook target URL:
curl -X PUT "https://api.hubapi.com/webhooks/v3/YOUR_APP_ID/settings" \
-H "Authorization: Bearer $HUBSPOT_TOKEN" \
-H "Content-Type: application/json" \
-d '{"targetUrl": "https://your-server.com/webhook"}'Polling alternative
If you can't host a webhook, poll for recently created unassigned contacts:
# Run every 5 minutes via cron
# */5 * * * * python poll_new_leads.py
import requests, os, json
from datetime import datetime, timedelta, timezone
HUBSPOT_TOKEN = os.environ["HUBSPOT_TOKEN"]
HEADERS = {"Authorization": f"Bearer {HUBSPOT_TOKEN}", "Content-Type": "application/json"}
five_min_ago = int((datetime.now(timezone.utc) - timedelta(minutes=5)).timestamp() * 1000)
resp = requests.post(
"https://api.hubapi.com/crm/v3/objects/contacts/search",
headers=HEADERS,
json={
"filterGroups": [{"filters": [
{"propertyName": "createdate", "operator": "GTE", "value": str(five_min_ago)},
{"propertyName": "hubspot_owner_id", "operator": "NOT_HAS_PROPERTY"}
]}],
"properties": ["firstname", "lastname", "email", "company", "jobtitle"],
"limit": 100
}
)
for contact in resp.json().get("results", []):
rep = get_next_rep() # Same function from above
# Assign and notify (same logic as webhook handler)
passCost
- Hosting: Free on Vercel (serverless), ~$5/mo on Railway
- Redis (for counter persistence): Free tier on Upstash or 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.