HubSpot Webhook Handler
Central inbound gateway for all HubSpot-originated events — deal stage changes, property updates, workflow action callbacks, and DocuSign envelope notifications. Handles 8+ registered paths under /webhook/hubspot plus alias /hubspot. On receipt, verifies HMAC signature, deduplicates, writes audit log, then fans out to: Phase 2 workflow action handlers (SMS consolidation, deal router, TC assignment, deal evaluator), Supabase stage sync, Gmail label auto-matching, Airtable template sync, dispo email dispatch, and the dispo-api sub-router.
Endpoint
| Path | Provider | Status |
|---|---|---|
https://webhook.reri.co/webhook/hubspot | HubSpot app 30738541 | ✅ approved |
https://webhook.reri.co/hubspot | HubSpot alias | ✅ approved |
https://webhook.reri.co/webhook/hubspot/actions | HubSpot UI Extension | ✅ approved |
https://webhook.reri.co/webhook/hubspot/actions/sms-consolidation | Phase 2 action | ✅ approved |
https://webhook.reri.co/webhook/hubspot/actions/deal-router | Phase 2 action | ✅ approved |
https://webhook.reri.co/webhook/hubspot/actions/tc-assignment | Phase 2 action | ✅ approved |
https://webhook.reri.co/webhook/hubspot/actions/deal-evaluator | Phase 2 action | ✅ approved |
https://webhook.reri.co/webhook/hubspot/api/dispo | Dispo Review API | ✅ approved |
https://webhook.reri.co/webhook/docusign | DocuSign Connect | ⚠️ approved-inactive (0 Connect configs live) |
Port: 18790 (systemd unit: hubspot-webhook.service)
Tunnel: Cloudflare Tunnel reri-api → webhook.reri.co (canonical); Tailscale Funnel as fallback
WAF: Cloudflare Pro custom rules on webhook.reri.co hostname (WAF header filter, not IP-range for HubSpot)
Auth method
Dual-version HMAC-SHA256 — tries v3 first, falls back to v1. Both use HUBSPOT_WEBHOOK_SECRET (env var from master.env).
v3 (preferred): HMAC-SHA256(secret, METHOD + URL + body + timestamp), base64-encoded in x-hubspot-signature-v3. Timestamp from x-hubspot-request-timestamp — rejects if >5 min old or <1 min future. Three URL candidates tried: forwarded-host, Tailscale FQDN, webhook.reri.co. Both raw and JSON-stringified body variants are tested (2×3 = 6 candidate strings checked per request).
v1 (fallback): SHA256(secret + rawBody) hex in x-hubspot-signature. Comparison via timingSafeEqual. HubSpot apps configured with x-hubspot-signature-version: v1 send only this variant — confirmed working via live traffic 2026-04-21.
Fail-open on unconfigured secret: if HUBSPOT_WEBHOOK_SECRET is unset, returns { valid: true, checked: false } (logs warning). Configure via master.env → op://Aurora/HubSpot/HUBSPOT_WEBHOOK_SECRET.
Payload shape
HubSpot sends an array of event objects per request. Key fields per event:
| Field | Type | Description |
|---|---|---|
subscriptionType | string | Event kind: deal.propertyChange, deal.creation, contact.creation, company.propertyChange, etc. |
objectId | number | HubSpot object ID (deal ID, contact ID, etc.) |
propertyName | string | For propertyChange events — which property changed |
propertyValue | string | New value of the changed property |
portalId | number | HubSpot portal/account ID |
appId | number | Originating HubSpot app ID (30738541 for main app) |
occurredAt | number | Event timestamp (ms epoch) |
eventId | string | Unique event ID — used as dedup key |
Phase 2 workflow action callbacks (/webhook/hubspot/actions/*) use a different shape — single object with inputFields (HubSpot CRM card action contract).
Downstream dispatch
hubspot-handler.js
├─ deal.propertyChange (dealstage)
│ ├─ stage-sync.js → Supabase acquisition_deals (bidirectional stage mirror)
│ ├─ dispo-deal-enricher.js → enriches deal from associated contacts
│ ├─ dispo-template-sync.js → HubSpot → Airtable → SalesMsg template auto-sync
│ ├─ dispo-email-sender.js → dispatch TC email on dispo stage transitions
│ └─ gmail-label-matcher.js → auto-match Gmail label to deal address
│
├─ deal.creation
│ ├─ gmail-label-creator.js → create address-named Gmail label subfolder
│ └─ stage-sync.js → Supabase write
│
├─ /webhook/hubspot/actions/sms-consolidation → actions/sms-consolidation.js
├─ /webhook/hubspot/actions/deal-router → actions/deal-router.js
├─ /webhook/hubspot/actions/tc-assignment → actions/tc-assignment.js
├─ /webhook/hubspot/actions/deal-evaluator → actions/deal-evaluator.js
│
└─ /webhook/hubspot/api/dispo/* → dispo-api.js sub-router
Owner-to-agent mapping (used for Gmail label subfolder naming):
38155416 → RE Resources · 76665119 → Henry Hill II · 159501174 → Angel · 85343098 → David · 87745573 → Ryan · 89080053 → Aurora
Dedup strategy
Uses checkDedup() from scripts/lib/webhook-dedup.js — writes event ID to processed_webhook_events Supabase table with a 24-hour TTL. If the same eventId arrives twice within the window, the handler returns 200 OK immediately (no re-processing). Dedup check happens AFTER signature verification and BEFORE any downstream dispatch.
Audit trail
webhook_audit_log— non-blocking write viaauditLog()fromscripts/lib/webhook-audit.js. Contains: path, provider, eventId, subscriptionType, objectId, timestamp, handler result.- Fallback: If Supabase is unreachable, falls back to
/tmp/webhook-audit-fallback.jsonllocal append. - Logger:
webhookLog(taggedhubspot-webhook) — structured JSON viascripts/lib/logger.js. Output goes tojournalctl --user -u hubspot-webhook. - Discord alerts: Not wired directly in this handler; errors surface via gateway health monitor →
#ops.
Related
- webhook-architecture — Cross-handler governance, dedup pattern, audit log schema, FUNNEL-REGISTRY invariants
- hubspot — HubSpot API reference, portal 30738541, deal pipeline IDs (acq 877963314 + dispo 816046), stage maps
- docusign — DocuSign Connect config, WAF IP allowlist (shared port 18790), envelope event shape
- _summary — Aurora is mapped to owner ID 89080053; Gmail label subfolders created under Aurora’s agent folder
- cron-timer-registry —
hubspot-webhook.servicesystemd unit, restart policy, port-registry entry - backend-model-map — How events dispatched from this handler route through the OpenClaw gateway to LLM tiers