Skip to main content

HubSpot

Audience: Sales, management, support, engineering · Where in app: Settings → Integrations → HubSpot · Plan availability: All plans (verify)

HubSpot is one of the two deep CRM integrations (alongside GoHighLevel). Once connected, every captured lead and conversation summary flows out to HubSpot Contacts, and HubSpot's view of that person — their lifecycle stage, open deals, tags, and recent notes — flows back in to enrich the identity graph so the AI knows who a returning visitor is. Returning visitors who were already tracked by HubSpot are recognized instantly via the hubspotutk cookie.

What it does

  • Syncs new leads from Knock Knock → HubSpot Contacts (create or update by email).
  • Pushes conversation summaries, chat transcripts, call logs, and page visits → HubSpot notes/timeline.
  • Auto-verifies returning visitors via the hubspotutk tracking cookie.
  • Fetches lifecycle stage, deals, tags, and notes back for the identity graph.
  • Answers B2B questions ("which HubSpot companies visited this week") via company lookups.

How it works

Auth model: OAuth. Connect from Settings → Integrations → HubSpot. The install URL points at app-na2.hubspot.com/oauth/authorize; after authorization the callback (GET /hubspot/install) stores on companies.hubspot.*:

  • access_token, refresh_token, access_token_expires_at
  • hub_id (the HubSpot portal id)
  • scopes (granted scopes)

Token refresh is automatic — the service refreshes via the refresh-token grant with a ~120-second expiry buffer.

Requested scopes: crm.objects.contacts.read/.write, crm.objects.companies.read/.write, crm.schemas.contacts.read/.write, crm.schemas.companies.read/.write, crm.lists.read, oauth. If a scope is missing post-connect, the integration shows a warning + reconnect prompt.

Direction: bi-directional.

HubSpot's tracking script sets a hubspotutk cookie on visitor browsers. When the Knock Knock widget loads, it reads the cookie and, if present, sends the value to the backend, which queries HubSpot for the matching contact. A match makes the session directly verified (an identity-graph direct trigger) — so a returning HubSpot-tracked visitor is recognized instantly, even without a form submission or landing-page identifier.

In the current backend code, raw form-intercept submissions enter via POST /api/intake (source='form_intercept') and are processed by FormSubmissionService.js, which does contact lookup/creation and identity resolution; the HubSpot service then operates on the resolved contact. Treat the exact hubspotutk-lookup wiring as verify against the live identity path.

Outbound sync (Knock Knock → HubSpot)

After a chat or call ends with a captured email, CrmSyncQueueService (BullMQ queue crm-sync, 3 workers, 3 retries w/ 5s exponential backoff) runs jobs:

JobWhat it does
sync-contactLook up contact by email; update properties (phone, last chat, lead score) or create it; persist hubspot_contact_id
sync-hubspot-chat-transcriptCreate/update a HubSpot note with the full chat transcript
sync-hubspot-call-logs / sync-hubspot-call-logCreate HubSpot call-log records for calls in the session
sync-hubspot-page-visitsLog page visits as HubSpot notes

Notes are created via the notes object (hs_note_body + hs_timestamp) and associated to the contact. Every API call is recorded in HubspotRequestModel (collection hubspot_requests: function_name, body, response, timestamps) for audit.

Inbound enrichment (HubSpot → Knock Knock)

For verified visitors with a HubSpot contact ID, CrmEnrichmentQueueService (queue crm-enrichment, 2 workers, 3 retries w/ 10s backoff) runs the refresh job (deduped per enrich:{person_id}:{company_id}). It pulls lifecycle stage, open deals, tags/properties, and recent notes, and caches them on PersonProfile.crm_enrichment (crm_enrichment.hubspot, crm_enrichment.refreshed_at). The cached "CRM State" is surfaced to the AI in its prompt. Refresh is event-driven (on identity changes / reconciliation), not on a fixed cron.

Company lookups (B2B)

getCompanyByDomain() and getCompanies() back the knox-crm tool for queries like "show me HubSpot companies that visited this week."

Configuration & options

ActionRoute
Get install URLGET /hubspot/install-url
OAuth callbackGET /hubspot/install
DisconnectDELETE /hubspot/uninstall

There are two service classes: HubspotService.js (a private-token client used for internal/admin operations) and PublicHubspotService.js (the per-tenant OAuth client).

Behaviors & edge cases

  • Enrichment-guessed contact push is off: LeadContactSyncJob — the cron that pushed enrichment-guessed (Clearbit/Apollo) contacts into HubSpot/GHL — is disabled globally (commented out of SchedulerService). Event-driven sync of real captured leads (CrmSyncQueueService) is unaffected, and hubspotutk landing detection is unchanged.
  • Sync timing: outbound sync is async — usually lands within ~60s. Check API Request Logs / hubspot_requests for sync-contact failures.
  • Upsert key: contacts are matched by email. If a contact already exists under a different email, you can get a mismatch — check for duplicate contacts.
  • Conflict handling: outbound updates overwrite the mapped HubSpot properties; the inbound cache is read-only enrichment.
  • Disconnect: wipes the tokens; historical syncs stay in HubSpot; future events stop syncing. crm_enrichment.hubspot_contact_id remains for reference but stops refreshing.
  • Historical backfill: syncing old chats is an admin-only backfill.

Plan & limits

  • Available on all plans (verify exact tier gating against package config).
  • High-volume tenants may hit HubSpot rate limits; the sync queues back off but don't drop work.

Technical implementation

  • Owning service: backendapp/services/HubspotService.js, PublicHubspotService.js; routes app/routes/hubspotRoutes.js; audit app/models/HubspotRequestModel.js.
  • Sync queues: CrmSyncQueueService.js (crm-sync) and CrmEnrichmentQueueService.js (crm-enrichment).
  • Subsystem: CRM & External Integration · Identity Graph.

What Nox can tell you

  • Connection state (token present + not expired) and last successful sync (via hubspot_requests).
  • Total contacts synced this period; failed syncs needing admin attention.
  • Whether a specific Person is in HubSpot (knox-crm(query=contact_in_hubspot, email)).

Tool: knox-crm.