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

PathProviderStatus
https://webhook.reri.co/webhook/hubspotHubSpot app 30738541✅ approved
https://webhook.reri.co/hubspotHubSpot alias✅ approved
https://webhook.reri.co/webhook/hubspot/actionsHubSpot UI Extension✅ approved
https://webhook.reri.co/webhook/hubspot/actions/sms-consolidationPhase 2 action✅ approved
https://webhook.reri.co/webhook/hubspot/actions/deal-routerPhase 2 action✅ approved
https://webhook.reri.co/webhook/hubspot/actions/tc-assignmentPhase 2 action✅ approved
https://webhook.reri.co/webhook/hubspot/actions/deal-evaluatorPhase 2 action✅ approved
https://webhook.reri.co/webhook/hubspot/api/dispoDispo Review API✅ approved
https://webhook.reri.co/webhook/docusignDocuSign Connect⚠️ approved-inactive (0 Connect configs live)

Port: 18790 (systemd unit: hubspot-webhook.service) Tunnel: Cloudflare Tunnel reri-apiwebhook.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.envop://Aurora/HubSpot/HUBSPOT_WEBHOOK_SECRET.

Payload shape

HubSpot sends an array of event objects per request. Key fields per event:

FieldTypeDescription
subscriptionTypestringEvent kind: deal.propertyChange, deal.creation, contact.creation, company.propertyChange, etc.
objectIdnumberHubSpot object ID (deal ID, contact ID, etc.)
propertyNamestringFor propertyChange events — which property changed
propertyValuestringNew value of the changed property
portalIdnumberHubSpot portal/account ID
appIdnumberOriginating HubSpot app ID (30738541 for main app)
occurredAtnumberEvent timestamp (ms epoch)
eventIdstringUnique 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 via auditLog() from scripts/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.jsonl local append.
  • Logger: webhookLog (tagged hubspot-webhook) — structured JSON via scripts/lib/logger.js. Output goes to journalctl --user -u hubspot-webhook.
  • Discord alerts: Not wired directly in this handler; errors surface via gateway health monitor → #ops.
  • 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-registryhubspot-webhook.service systemd unit, restart policy, port-registry entry
  • backend-model-map — How events dispatched from this handler route through the OpenClaw gateway to LLM tiers