Skip to Content
Part 3: IntegrationsCh 12: Salesforce

Salesforce integration follows the same architectural pattern as HubSpot — OAuth, token management, paginated fetching, batch upsert — but against a different API surface. This chapter focuses on the differences rather than repeating the shared patterns from Chapter 11.

The Same Pattern, Different API

Both CRM integrations share:

  • OAuth 2.0 authorization flow
  • Token storage in crm_connections (provider = ‘salesforce’)
  • Automatic token refresh before expiry
  • Batch company/contact/deal sync using UNNEST
  • Entity resolution by provider ID and email domain

The key differences:

AspectHubSpotSalesforce
API StyleREST with pagination cursorsREST + SOQL query language
Auth URLapp.hubspot.com/oauth/authorizelogin.salesforce.com/services/oauth2/authorize
Token URLapi.hubapi.com/oauth/v1/tokenlogin.salesforce.com/services/oauth2/token
Entity NamesCompanies, Contacts, DealsAccounts, Contacts, Opportunities
Property AccessProperty name stringField API name
Webhook EventsSubscription-based (deal.propertyChange)Change Data Capture (OpportunityChangeEvent)
Employee Fieldnumberofemployees (integer)NumberOfEmployees (integer)
Revenue Fieldannualrevenue (number)AnnualRevenue (currency)

SOQL: Salesforce’s Query Language

HubSpot uses paginated REST endpoints. Salesforce uses SOQL (Salesforce Object Query Language) — a SQL-like language for querying CRM data:

SELECT Id, Name, Website, Industry, NumberOfEmployees, AnnualRevenue, BillingCountry, BillingState, BillingCity, Phone, Description FROM Account WHERE LastModifiedDate > 2024-01-01T00:00:00Z ORDER BY LastModifiedDate DESC LIMIT 200

This is more flexible than HubSpot’s property-based fetching — you can filter, sort, and join in the query itself. But it also means the sync service needs to construct valid SOQL strings, which introduces SQL-injection-like risks. All user input is escaped before being placed in SOQL queries.

Webhook Format: Change Data Capture

Salesforce webhooks use Change Data Capture (CDC) events, which are structurally different from HubSpot’s subscription events:

// HubSpot event: { subscriptionType: 'deal.propertyChange', objectId: 12345, propertyName: 'dealstage', propertyValue: 'closedwon' } // Salesforce CDC event: { ChangeEventHeader: { entityName: 'Opportunity', changeType: 'UPDATE', changedFields: ['StageName', 'Amount'], recordIds: ['006xx000001abc'], commitTimestamp: 1711234567000 } }

The alert evaluation service normalizes both into a common format (Chapter 15), so the trigger matching logic doesn’t need to know which CRM the event came from.

Entity Name Mapping

Salesforce uses different terminology than HubSpot and Astrelo:

Salesforce → Astrelo → Database Table ──────────────────────────────────────────────── Account → Company → companies Contact → Contact → contacts Opportunity → Deal → deals OpportunityContactRole → Deal Contact → deal_contacts Task → Task → tasks

The sync service handles this mapping transparently. From the scoring engine’s perspective, a company is a company whether it came from HubSpot or Salesforce.

Multi-CRM Support

A user can only have one active CRM connection at a time. The crm_connections table enforces this with a UNIQUE(user_id, provider) constraint. If a user switches from HubSpot to Salesforce, the old connection is deactivated (is_active = false) and a new one is created.

The scoring engine doesn’t care which CRM is connected — it reads from companies, contacts, and deals regardless of source. The external_source column on each record tracks the origin (‘hubspot’ or ‘salesforce’) for sync conflict resolution.

Key Takeaways

  1. Same architecture, different API. Both CRMs follow the OAuth → Sync → Score pipeline.
  2. SOQL vs REST pagination is the biggest technical difference.
  3. CDC events provide richer change information than HubSpot’s property-change subscriptions.
  4. Entity mapping is handled at the sync layer — the rest of the app is CRM-agnostic.

Next chapter: Slack integration — how alerts get delivered outside the app.

Last updated on