Canonical
HubSpot
Push Orakel company records into HubSpot via OAuth, with webhook-driven enrichment on org_number change.
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_numberchanges, HubSpot calls Orakel; Orakel fetches the enriched record (Brreg, Regnskapsregisteret, roles, domains) andPATCHes the company back. - Batch push:
POST /api/push/:destinationNameupserts 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>.
- Open the invite URL while logged into the target HubSpot portal. Single-use.
- Authorize. Tick "I understand the risks of connecting an unverified app" and click Connect app.
- 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. - Test. Open any Company record, set
Organisasjonsnummerto 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. - Optional backfill. Click ENRICH MY EXISTING COMPANIES on the CONNECTED page to enrich every company with
org_numberset. Available for 30 minutes after install.
OAuth flow
GET /api/oauth/hubspot/installsets a state cookie and redirects tohttps://app-eu1.hubspot.com/oauth/authorize.GET /api/oauth/hubspot/callbackexchanges the code, introspectsportalId, creates aDestinationnamedhubspot-<portalId>, and provisions the custom properties.- Refresh tokens are rotated on every refresh;
lib/hubspot/client.tshandles it on401.
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": {}
}portalIdis the HubSpot hub ID; used as the match key when looking up the destination by portal.fieldMappingsis reserved for future custom slugs and unused today.
Push behavior
- Match: search
/crm/v3/objects/companies/searchfororg_number = <orgNumber>,limit: 1. - Update:
PATCH /crm/v3/objects/companies/<id>with the properties map. - Create:
POST /crm/v3/objects/companieswith the same properties. - Webhook:
company.creationandcompany.propertyChange(propertyorg_number) subscriptions triggerenrichHubspotCompany(portalId, objectId)asynchronously. The webhook endpoint returns200 { ok: true, processing: [...] }immediately. - Signature: v3 HMAC-SHA256 over
METHOD + public URI + body + timestamp. The public URI is reconstructed fromX-Forwarded-ProtoandX-Forwarded-Hostso signing matches behind the reverse proxy.
Gotchas
industryis a 147-value enum. Pushing Norwegian NACE text returns400 INVALID_OPTION. Orakel writes NACE to a customnace_industrytext field; HubSpot's built-inindustryis untouched.- Boolean properties need explicit options.
is_bankruptis provisioned with thetrue/falsepair or HubSpot returnsINVALID_BOOLEAN_OPTION. - Brand names preserved — webhook enrichment does not overwrite
nameon 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.
Related
- Destinations endpoint — CRUD for destinations and push trigger
- Brreg — primary source for firmographics and roles
- Regnskapsregisteret — source for the financial fields