orakel
Docs navigation

Canonical

HubSpot

Push Orakel company records into HubSpot via OAuth, with webhook-driven enrichment on org_number change.

Updated 2026-04-21

Overview

HubSpot is a CRM. Orakel installs as an OAuth app inside a HubSpot portal. Two flows run once installed:

  • Webhook enrichment: when a Company is created or org_number changes, HubSpot calls Orakel; Orakel fetches the enriched record (Brreg, Regnskapsregisteret, roles, domains) and PATCHes the company back.
  • Batch push: POST /api/push/:destinationName upserts a list of Norwegian org numbers.

OAuth tokens and the portal ID are stored as a Destination of type hubspot, encrypted at rest (AES-256-GCM). Access tokens refresh automatically on 401. The app is distributed as an unlisted HubSpot Projects app (platform 2026.03); installs happen via direct URL.

Setup

The installer receives a one-time invite URL — https://orakel.cloud/hubspot/invite/<token>.

  1. Open the invite URL while logged into the target HubSpot portal. Single-use.
  2. Authorize. Tick "I understand the risks of connecting an unverified app" and click Connect app.
  3. Land on the CONNECTED page. Orakel has requested crm.objects.companies.read/write + crm.schemas.companies.read/write, provisioned nine custom properties on the Company object, and stored encrypted tokens.
  4. Test. Open any Company record, set Organisasjonsnummer to a 9-digit org number (e.g. 923609016), save. Within 5–10 seconds Orakel fills in name, address, NACE, employees, revenue, CEO, and the rest.
  5. Optional backfill. Click ENRICH MY EXISTING COMPANIES on the CONNECTED page to enrich every company with org_number set. Available for 30 minutes after install.

OAuth flow

  • GET /api/oauth/hubspot/install sets a state cookie and redirects to https://app-eu1.hubspot.com/oauth/authorize.
  • GET /api/oauth/hubspot/callback exchanges the code, introspects portalId, creates a Destination named hubspot-<portalId>, and provisions the custom properties.
  • Refresh tokens are rotated on every refresh; lib/hubspot/client.ts handles it on 401.

Field mapping

Orakel pushes these properties. Custom properties are provisioned on first install; HubSpot-native ones (name, domain, phone, address, city, zip, numberofemployees, annualrevenue, founded_year, linkedin_company_page) are written as-is.

Orakel field HubSpot property Notes
name name Not overwritten on existing records via the webhook path — HubSpot handles updates.
orgNumber org_number Custom. Match key.
primaryDomain or website domain Primary enrichment domain wins.
phone phone
businessAddressStreet address
businessAddressCity city
businessAddressZip zip
employeeCount numberofemployees
financials[0].revenue annualrevenue, revenue_nok Both written. Custom revenue_nok labelled in NOK.
financials[0].netResult net_result_nok Custom. NOK.
financials[0].operatingResult operating_result_nok Custom. NOK.
financials[0].totalEquity total_equity_nok Custom. NOK.
CEO from roles[] (DAGL) ceo_name Custom. From Brreg roles.
naceDescription1 nace_industry Custom. Norwegian NACE text — HubSpot's built-in industry enum is not written.
orgFormCode org_form Custom. AS, ASA, ENK, etc.
isBankrupt is_bankrupt Custom boolean.
foundingDate year founded_year Built-in.
linkedinHandle linkedin_company_page Built-in. Full URL.

Null values are dropped before PATCH.

Configuration

The callback writes this config shape (encrypted) — you do not author it by hand:

{
  "accessToken": "...",
  "refreshToken": "...",
  "expiresAt": 1713705600000,
  "portalId": 147637517,
  "portalName": "Acme Portal",
  "userEmail": "installer@acme.example",
  "fieldMappings": {}
}
  • portalId is the HubSpot hub ID; used as the match key when looking up the destination by portal.
  • fieldMappings is reserved for future custom slugs and unused today.

Push behavior

  • Match: search /crm/v3/objects/companies/search for org_number = <orgNumber>, limit: 1.
  • Update: PATCH /crm/v3/objects/companies/<id> with the properties map.
  • Create: POST /crm/v3/objects/companies with the same properties.
  • Webhook: company.creation and company.propertyChange (property org_number) subscriptions trigger enrichHubspotCompany(portalId, objectId) asynchronously. The webhook endpoint returns 200 { ok: true, processing: [...] } immediately.
  • Signature: v3 HMAC-SHA256 over METHOD + public URI + body + timestamp. The public URI is reconstructed from X-Forwarded-Proto and X-Forwarded-Host so signing matches behind the reverse proxy.

Gotchas

  • industry is a 147-value enum. Pushing Norwegian NACE text returns 400 INVALID_OPTION. Orakel writes NACE to a custom nace_industry text field; HubSpot's built-in industry is untouched.
  • Boolean properties need explicit options. is_bankrupt is provisioned with the true/false pair or HubSpot returns INVALID_BOOLEAN_OPTION.
  • Brand names preserved — webhook enrichment does not overwrite name on existing records.
  • Unlisted-app warning at install is expected. The app is unreviewed, not unsafe.
  • One destination per portal. Tokens are portal-scoped.
  • Backfill window is 30 minutes post-install. After that, re-issue an invite.
  • Norwegian companies only. Finland and Sweden extend once Nordic phase 1 ships.