Instantly notify a rep in Slack when a high-intent lead books a demo using code
medium complexityCost: $0
Prerequisites
Prerequisites
- Node.js 18+ or Python 3.9+
- HubSpot private app token with
crm.objects.contacts.readandformsscopes - Slack Bot Token (
xoxb-...) withchat:writescope - A server or serverless function to receive webhooks (Express, Vercel, AWS Lambda)
- A mapping of HubSpot owner IDs to Slack user IDs
Step 1: Subscribe to form submissions
Register a webhook subscription for form submissions in HubSpot:
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.propertyChange",
"propertyName": "recent_conversion_event_name",
"active": true
}'Form submission detection
HubSpot doesn't have a direct "form submitted" webhook event type. Instead, subscribe to the recent_conversion_event_name property change, which updates whenever a form is submitted. Filter for your demo form's name in the handler.
Step 2: Build the webhook handler
from flask import Flask, request, jsonify
from slack_sdk import WebClient
import requests
import os
app = Flask(__name__)
slack = WebClient(token=os.environ["SLACK_BOT_TOKEN"])
HUBSPOT_TOKEN = os.environ["HUBSPOT_TOKEN"]
HEADERS = {"Authorization": f"Bearer {HUBSPOT_TOKEN}"}
PORTAL_ID = os.environ.get("HUBSPOT_PORTAL_ID", "YOUR_PORTAL_ID")
DEMO_FORM_NAME = "Book a Demo" # Match your form's name
OWNER_TO_SLACK = {
"12345678": "U01AAAA",
"23456789": "U02BBBB",
"34567890": "U03CCCC",
"45678901": "U04DDDD",
}
FALLBACK_CHANNEL = os.environ.get("SLACK_FALLBACK_CHANNEL", "#demo-alerts")
@app.route("/webhook", methods=["POST"])
def handle_webhook():
events = request.json
for event in events:
# Filter for demo form submissions
if event.get("propertyName") != "recent_conversion_event_name":
continue
if DEMO_FORM_NAME.lower() not in (event.get("propertyValue") or "").lower():
continue
contact_id = event["objectId"]
# Fetch full contact
resp = requests.get(
f"https://api.hubapi.com/crm/v3/objects/contacts/{contact_id}",
headers=HEADERS,
params={"properties": "firstname,lastname,email,jobtitle,company,numberofemployees,industry,hubspot_owner_id,hs_analytics_source"}
)
resp.raise_for_status()
props = resp.json()["properties"]
# Resolve Slack target
owner_id = props.get("hubspot_owner_id")
slack_target = OWNER_TO_SLACK.get(owner_id, FALLBACK_CHANNEL)
name = f"{props.get('firstname', '')} {props.get('lastname', '')}".strip()
slack.chat_postMessage(
channel=slack_target,
text=f"Demo booked: {name} at {props.get('company', 'Unknown')}",
blocks=[
{"type": "header", "text": {"type": "plain_text", "text": "🔥 Demo Booked!"}},
{"type": "section", "fields": [
{"type": "mrkdwn", "text": f"*Name:*\n{name}"},
{"type": "mrkdwn", "text": f"*Title:*\n{props.get('jobtitle', 'N/A')}"},
{"type": "mrkdwn", "text": f"*Company:*\n{props.get('company', 'Unknown')} ({props.get('numberofemployees', '?')} employees)"},
{"type": "mrkdwn", "text": f"*Industry:*\n{props.get('industry', 'Unknown')}"},
{"type": "mrkdwn", "text": f"*Source:*\n{props.get('hs_analytics_source', 'Unknown')}"},
{"type": "mrkdwn", "text": f"*Email:*\n{props.get('email', 'N/A')}"},
]},
{"type": "actions", "elements": [
{"type": "button", "text": {"type": "plain_text", "text": "View Contact"},
"url": f"https://app.hubspot.com/contacts/{PORTAL_ID}/contact/{contact_id}",
"style": "primary"}
]}
]
)
return jsonify({"status": "ok"}), 200Step 3: Deploy and register the webhook URL
Deploy your handler, then register it with HubSpot:
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"}'Block Kit field limit
Slack's section block supports a maximum of 10 fields. The example uses 6. If you add more context (lead score, lifecycle stage, etc.), stay under the limit or split into multiple section blocks.
Polling alternative
If you can't host a webhook, poll for recent form submissions:
# Run every 5 minutes via cron
# */5 * * * * python poll_demo_bookings.py
import requests, os
from datetime import datetime, timedelta, timezone
from slack_sdk import WebClient
HUBSPOT_TOKEN = os.environ["HUBSPOT_TOKEN"]
HEADERS = {"Authorization": f"Bearer {HUBSPOT_TOKEN}", "Content-Type": "application/json"}
slack = WebClient(token=os.environ["SLACK_BOT_TOKEN"])
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": "recent_conversion_date", "operator": "GTE", "value": str(five_min_ago)}
]}],
"properties": ["firstname", "lastname", "email", "jobtitle", "company",
"numberofemployees", "industry", "hubspot_owner_id",
"recent_conversion_event_name"],
"limit": 100
}
)
for contact in resp.json().get("results", []):
if "demo" in (contact["properties"].get("recent_conversion_event_name") or "").lower():
# Send Slack alert (same Block Kit as above)
passCost
- 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.