HubSpot Webhooks to a Private Lead Scoring Service Using Skooh
Build an internal lead scoring engine and sync scores back to HubSpot automatically.
Use Case
Your internal lead scoring uses proprietary signals:
- Product usage data
- Industry intelligence
- Firmographic data from internal sources
You want to score leads internally and push scores to HubSpot for your sales team.
Architecture
HubSpot (contact.propertyChange) → Skooh → Lead Scoring Engine
↓
Internal Data Sources
↓
HubSpot API (update score)
Setup
1. Create Skooh Endpoint
Create "HubSpot Lead Scoring" in Skooh.
2. Run the Skooh Proxy
Using the Desktop App:
- Download the Skooh Desktop App
- Enter your API key and hook token
- Set forwarding URL (e.g., `http://localhost:3000/webhooks/hubspot\`)
- Click Connect
Using Docker:
docker run -d \\
--name skooh-hubspot \\
-e SKOOH_TOKEN=hubspot-leads \\
-e SKOOH_API_KEY=your_api_key \\
-e FORWARD_URL=http://lead-scoring:3000/webhooks/hubspot \\
--restart unless-stopped \\
skooh/proxy
3. Configure HubSpot Webhook
Note: See HubSpot's Webhook documentation for current setup instructions.
In HubSpot:
- Go to Settings → Integrations → Private Apps
- Create a private app with webhook subscriptions
- Subscribe to `contact.propertyChange` events
- Point to your Skooh URL
The Lead Scoring Handler
const hubspot = require('@hubspot/api-client');
const hubspotClient = new hubspot.Client({ accessToken: process.env.HUBSPOT_TOKEN });
app.post('/webhooks/hubspot', express.json(), async (req, res) => {
const events = req.body;
for (const event of events) {
if (event.subscriptionType === 'contact.propertyChange') {
await handleContactChange(event);
}
}
res.status(200).send('OK');
});
async function handleContactChange(event) {
const contactId = event.objectId;
const propertyName = event.propertyName;
const propertyValue = event.propertyValue;
// Re-score on relevant property changes
const scoreTriggers = ['email', 'company', 'jobtitle', 'industry'];
if (scoreTriggers.includes(propertyName)) {
await rescoreContact(contactId);
}
}
The Scoring Engine
async function rescoreContact(contactId) {
// Fetch contact from HubSpot
const contact = await hubspotClient.crm.contacts.basicApi.getById(contactId, [
'email', 'company', 'jobtitle', 'industry', 'website'
]);
// Calculate score from multiple sources
const score = await calculateLeadScore(contact.properties);
// Update HubSpot with new score
await hubspotClient.crm.contacts.basicApi.update(contactId, {
properties: {
lead_score: score.total.toString(),
lead_score_breakdown: JSON.stringify(score.breakdown),
lead_score_updated: new Date().toISOString()
}
});
// High score? Notify sales
if (score.total >= 80) {
await notifySalesTeam(contact, score);
}
}
async function calculateLeadScore(properties) {
let score = 0;
const breakdown = {};
// 1. Firmographic score (from internal DB)
const firmographic = await firmographicDb.lookup(properties.company);
if (firmographic) {
const firmScore = scoreFirmographic(firmographic);
score += firmScore;
breakdown.firmographic = firmScore;
}
// 2. Job title score
const titleScore = scoreJobTitle(properties.jobtitle);
score += titleScore;
breakdown.title = titleScore;
// 3. Industry fit
const industryScore = scoreIndustry(properties.industry);
score += industryScore;
breakdown.industry = industryScore;
// 4. Product usage (if they've signed up)
const usage = await usageDb.findByEmail(properties.email);
if (usage) {
const usageScore = scoreUsage(usage);
score += usageScore;
breakdown.usage = usageScore;
}
// 5. Intent signals (from marketing data)
const intent = await intentDb.findByDomain(extractDomain(properties.email));
if (intent) {
const intentScore = scoreIntent(intent);
score += intentScore;
breakdown.intent = intentScore;
}
return { total: Math.min(score, 100), breakdown };
}
function scoreJobTitle(title) {
if (!title) return 0;
const lower = title.toLowerCase();
// Decision makers
if (lower.includes('cto') || lower.includes('vp') || lower.includes('director')) {
return 20;
}
// Technical leads
if (lower.includes('architect') || lower.includes('lead') || lower.includes('manager')) {
return 15;
}
// Engineers
if (lower.includes('engineer') || lower.includes('developer')) {
return 10;
}
return 5;
}
Verifying HubSpot Webhooks
const crypto = require('crypto');
function verifyHubSpotSignature(req) {
const signature = req.headers['x-hubspot-signature-v3'];
const timestamp = req.headers['x-hubspot-request-timestamp'];
const uri = req.protocol + '://' + req.get('host') + req.originalUrl;
const message = req.method + uri + JSON.stringify(req.body) + timestamp;
const hash = crypto
.createHmac('sha256', process.env.HUBSPOT_CLIENT_SECRET)
.update(message)
.digest('base64');
return hash === signature;
}
Testing Lead Scoring
- Update a contact in HubSpot
- Find the webhook in Skooh
- Replay to test scoring
- Check that lead_score updated in HubSpot
Written by Skooh Team