CRM systems
Introduction to CRM Data Models and APIs: HubSpot & Salesforce
Modern CRM platforms like Salesforce and HubSpot serve as the system of record for customer and sales data. Understanding their core data models and APIs is essential for any SaaS product integrating with these systems — whether for syncing customer information, automating sales processes, or enabling data-driven workflows.
This guide introduces the key CRM objects, their relationships, and the API approaches used in Salesforce and HubSpot.
Core CRM Objects
Both Salesforce and HubSpot organize customer data around standard business entities, referred to as "Objects." These objects map directly to common concepts in sales and customer management.
| CRM Object | Salesforce | HubSpot | Purpose |
|---|---|---|---|
| Account | Account | Company | Represents a business or organization. |
| Contact | Contact | Contact | Represents an individual person (prospect, lead, or customer). |
| Lead* | Lead* | Contact (Lifecycle Stage: Lead) | Prospective customer not yet qualified. Salesforce treats as a separate object; HubSpot uses a stage within Contacts. |
| Opportunity / Deal | Opportunity | Deal | Represents a potential revenue-generating event (sale, renewal, upsell). |
| Activity / Engagement | Task / Event / Email | Engagement | Records customer interactions like calls, emails, meetings. |
| Custom Objects | Custom Objects | Custom Objects | Allows modeling of additional entities unique to your business (subscriptions, assets, tickets, etc). |
*Note that some of our clients might use Leads to store at least proportion of their "contact" records. However, given that Leads are not associated to the Accounts by default we've made decision to only sync to Contact object.
Relationships Between Objects
CRMs use well-defined object relationships to reflect real-world associations between people, companies, and sales processes.
| Relationship | Description | Example |
|---|---|---|
| Account ⇄ Contact | Many Contacts belong to a single Account (Company). | Employees of a customer organization. |
| Contact ⇄ Deal | Contacts can be associated with multiple Deals. | A buyer involved in several sales cycles. |
| Account ⇄ Deal | An Account (Company) may have multiple Deals. | Multiple opportunities. |
| Contact ⇄ Activity | Activities log interactions for a specific Contact. | Calls, emails, meetings tracked over time. |
| Account ⇄ Activity | Activities can be associated with Accounts as well. | Calls, emails, meetings tracked over time. |
API Access and Integration
Both Salesforce and HubSpot offer comprehensive APIs for programmatic access to CRM data, enabling SaaS products to read, write, and sync customer data at scale.
Salesforce API Overview
Salesforce provides a highly flexible and powerful API ecosystem:
- REST API: Standard CRUD operations on objects. → Salesforce REST API Overview
- Bulk API: Optimized for processing large data volumes. → Salesforce Bulk API Guide
- SOQL: Salesforce Object Query Language for advanced querying. → SOQL Reference
- Metadata API: Programmatically access object schemas and configurations. → Salesforce Metadata API
- Authentication: OAuth 2.0 standard for secure integrations. → Salesforce Authentication Guide
HubSpot API Overview
HubSpot offers a simpler, more opinionated API structure optimized for ease of use:
- CRM API: CRUD operations for standard objects (Contacts, Companies, Deals). → HubSpot CRM API Overview
- Associations API: Manage relationships between objects. → HubSpot Associations API
- Custom Objects API: Create and manage custom entities. → HubSpot Custom Objects API
- Webhooks API: Real-time notifications on CRM events. → HubSpot Webhooks API
- Authentication: OAuth 2.0 for secure access. → HubSpot OAuth Guide
- Default company properties → Default properties
Common fields and relationships
Company (Account) Object — Key Fields Comparison
| Concept / Purpose | Salesforce Field Name | HubSpot Property Name | Notes |
|---|---|---|---|
| Company Name | Name | name | Primary identifier of the company/account. |
| Website / Domain | Website | domain | Critical for matching, enrichment, deduplication. Salesforce uses full URL; HubSpot uses root domain (e.g., example.com). Note that it is not uncommon for a client to maintain a custom domain field in Salesforce. |
| CRM Owner | OwnerId (User reference) | hubspot_owner_id | The user that owns the account. |
| Last Activity Date | LastActivityDate | hs_last_engagement_date | Auto-updated on engagement (email, call, meeting, etc). |
| Last Modified Date | LastModifiedDate | hs_lastmodifieddate | Tracks when record was last updated. |
| Lifecycle Stage / Status | Custom or derived from Opportunities | lifecyclestage | In HubSpot, lifecycle stage is a built-in property that auto-updates based on deal creation/close. In Salesforce, this often requires configuration. |
| Number of Deals | Roll-Up Summary Field (Count of Opportunities) | num_associated_deals | HubSpot auto-calculates; Salesforce uses Roll-Up Summary Fields. |
| Recent Deal Amount | Roll-Up Summary Field (Sum of Opportunity Amount) | recent_deal_amount | HubSpot built-in; Salesforce requires Roll-Up Summary Field. |
Contact (Person) Object — Key Fields Comparison
| Concept / Purpose | Salesforce Field Name | HubSpot Property Name | Notes |
|---|---|---|---|
| First Name | FirstName | firstname | Core identity fields. |
| Last Name | LastName | lastname | Core identity fields. |
| Common integration identifier. | |||
| Phone Number | Phone | phone | Primary phone number. |
| Associated Company | AccountId | associatedcompanyid | Relationship to Account/Company object. |
| CRM Owner | OwnerId | hubspot_owner_id | Assigned user responsible for the contact. |
| Lead Source | LeadSource | lead_source | Origin of the lead/contact. Useful for attribution. |
| Lifecycle Stage | Custom or Lead Object | lifecyclestage | HubSpot uses a built-in property. Salesforce uses separate Lead object or custom field. |
| Last Activity Date | LastActivityDate | hs_last_engagement_date | Auto-updated on engagement (email, call, meeting, etc). |
| Last Modified Date | LastModifiedDate | hs_lastmodifieddate | Tracks when record was last updated. |
| Number of Deals | Roll-Up Summary Field (Count of Opportunities) | num_associated_deals | HubSpot built-in; Salesforce requires Roll-Up Summary Field. |
Notes on Field Behavior
| Field Type | HubSpot | Salesforce |
|---|---|---|
| Auto-Populated Properties | Extensive — lifecyclestage, num_associated_deals, hs_last_engagement_date auto-update without custom config. | Standard Fields like LastActivityDate exist, but deal counts and revenue require Roll-Up Summary Fields (setup dependent). Often, the companies maintain their own fields rather than using defaults. |
| Customization | Property model — easy to add custom fields per object. User can add roll-up fields (i.e. copying data from Deal → Company) via workflows. | Highly customizable schema — supports custom fields, roll-ups, and logic via automation tools. |
| Relationship Management | Associations API to link Contacts, Companies, Deals, Activities. | Relational database model with lookup fields and junction objects. |
How to Resolve OwnerId (Salesforce) and hubspot_owner_id (HubSpot) to Owner Name
Both Salesforce and HubSpot store the Owner of a record (Account/Contact/Deal) as an internal ID reference — not the user’s name or email directly.
To display or sync the name or email of the record owner, you need to perform an additional API lookup to the Users object (Salesforce) or Owners object (HubSpot).
Salesforce: Resolving OwnerId
Field
| Field Name | Stored Value | Notes |
|---|---|---|
| OwnerId | Salesforce User ID (e.g., 005ABC123456789XYZ) | Reference to a User record. |
How To Fetch Owner Name
Use the Salesforce API to fetch the User object:
GET /services/data/vXX.X/sobjects/User/{OwnerId}
Response will include:
| Field | Notes |
|---|---|
| Name | Full name of the user. |
| Email address of the user. | |
| Username | Internal Salesforce username (may be email or custom). |
Alternatively, in SOQL:
SELECT Id, Name, Email FROM User WHERE Id = '005ABC123456789XYZ'
And can ‘join’ (aka traverse relationship) like:
SELECT
Id,
Name,
Industry,
Website,
OwnerId,
Owner.Name,
Owner.Email
FROM Account
WHERE Website != NULL
LIMIT 100
Owner.Name and Owner.Email can be included directly when fetching Account, Contact, Opportunity via REST API
HubSpot: Resolving hubspot_owner_id
Field
| Field Name | Stored Value | Notes |
|---|---|---|
| hubspot_owner_id | HubSpot Owner ID (UUID format) | Reference to an Owner record. |
How To Fetch Owner Name
Use the HubSpot Owners API:
GET https://api.hubapi.com/crm/v3/owners/{ownerId}
Response will include:
| Field | Notes |
|---|---|
| firstName / lastName | Owner’s name. |
| Owner’s email address. | |
| id | Internal ID (matches hubspot_owner_id). |
HubSpot Owners API Reference:
https://developers.hubspot.com/docs/api/crm/owners
Summary
| CRM | Field in Record | API to Resolve | Output Fields |
|---|---|---|---|
| Salesforce | OwnerId | /sobjects/User/{Id} | Name, Email |
| HubSpot | hubspot_owner_id | /crm/v3/owners/{id} | firstName, lastName, email |
Best Practices for Integrations
- Cache Owner Lookups (might only be needed for Hubspot)
- Owners don’t change often → Cache User/Owner info locally.
- Could have a write through cache table?
- Store Owner Name & Email Locally
- When syncing data from Salesforce/HubSpot → Store both ID + Owner Name/Email in your system for fast display.
- Avoid Relying on ID in Front-End
- Always resolve to human-friendly name or email before displaying in UI or notifications.
Handling Multi-Select Fields in Salesforce & HubSpot APIs
Salesforce — Multi-Picklist Fields
- Multi-select fields (e.g.,
Industries__c) are returned as a single string with values separated by a;(semicolon). - Example Response:
"Industries__c": "Technology;Healthcare;Finance"
- Integration Handling:
- Split the string client-side:
industries = response['Industries__c'].split(';')
HubSpot — Multi-Checkbox Fields
- Multi-select fields (e.g.,
industry) are returned as a JSON array of strings. - Example Response:
"industry": ["technology", "healthcare", "finance"]
- Integration Handling:
- Use directly as an array — no parsing needed.
Summary
| CRM | API Response | Integration Handling |
|---|---|---|
| Salesforce | String with ; delimiter | Split string by ; to array. |
| HubSpot | JSON Array | Use array as-is. |
Key Field-Type Gotchas in Salesforce & HubSpot
1. Multi-Select Fields
(Already covered — but worth reiterating)
| CRM | Behavior | Gotcha |
|---|---|---|
| Salesforce | String with ; separator | Must split manually in code. |
| HubSpot | Array of strings | Safe — native array. |
2. Date & DateTime Fields
| CRM | Behavior | Gotcha |
|---|---|---|
| Salesforce | ISO8601 but Date vs DateTime are different fields | Date is date-only (no timezone), DateTime is full timestamp. Watch for timezone issues — stored in UTC. |
| HubSpot | Always timestamp in milliseconds since epoch | Must convert from ms to ISO or Date object in your app. Easily missed! Example: 1673455500000 = UTC datetime. |
3. Boolean / Checkbox Fields
| CRM | Behavior | Gotcha |
|---|---|---|
| Salesforce | Boolean true / false values | Safe — native boolean values in API. |
| HubSpot | Stored as string "true" / "false" in API | Must cast to real boolean in your code. |
4. Picklist / Dropdown Fields
| CRM | Behavior | Gotcha |
|---|---|---|
| Salesforce | Returns stored value — not label | Often a machine-friendly value — need to map to label if displaying to users. Labels available via describe API. |
| HubSpot | Returns stored value (not label) | Same — you must map stored values to display labels from the properties API metadata. |
5. Owner / User Fields
| CRM | Behavior | Gotcha |
|---|---|---|
| Salesforce | OwnerId requires relationship query (Owner.Name) | Fetch via SOQL or fields=Owner.Name. Otherwise, second API call needed. |
| HubSpot | hubspot_owner_id — must fetch Owner separately | No inline Owner object. Prefetch and cache Owners for efficiency. |
6. Number Fields
| CRM | Behavior | Gotcha |
|---|---|---|
| Salesforce | Safe — returns numeric value | Handle null/empty values gracefully. |
| HubSpot | Always stored as string in API | Must cast to number in code. Even numeric fields like annualrevenue come back as strings. |
7. Null / Empty Field Behavior
| CRM | Behavior | Gotcha |
|---|---|---|
| Salesforce | Returns field as null | Straightforward — handle null checks. |
| HubSpot | Field may be missing from properties entirely | Must check for existence of key in the response before accessing value. |
Summary
| Field Type | Salesforce | HubSpot | Integration Tip |
|---|---|---|---|
| Multi-select | ; separated string | Array | Normalize to array in both cases. |
| Date | ISO, UTC | Epoch ms | Always convert to ISO Date object. |
| Boolean | True boolean | String "true" / "false" | Normalize to boolean in code. |
| Picklist | Stored value | Stored value | Map to label via schema API. |
| Owner | OwnerId (relationship query) | hubspot_owner_id (extra API call) | Prefetch and cache Owners or use Owner.Name (SFDC) |
| Number | Native number | String | Cast to number. |
| Null | null value | Field may be missing | Use .get() or null checks. |
Salesforce 15 vs 18 char length IDs
Salesforce uses 15-character IDs for internal use (e.g. in the UI) and 18-character IDs for external systems because the 15-character version is case-sensitive, which can cause issues in systems like Excel or databases that ignore case. The 18-character ID adds a 3-character checksum to make the ID case-insensitive safe — both refer to the same record. To convert a 15-character ID to 18, you can use https://github.com/GoodFit-io/gf-sourcers/blob/main/src/services/enrichmentApi/utils/sfdc.ts#L1; to go back, just take the first 15 characters.
Whenever we accept an ID we should accept both 15 and 18 chars, but only store 18 char versions in DB.
Soft deletion
Both CRMs support soft deletion:
- salesforce = recycle bin
- hubspot = archived
| CRM | Soft Delete Mechanism | Are Soft Deletes Returned by Default? | How To Fetch Soft Deleted / Archived Records | Best Practice for Detecting Deletes |
|---|---|---|---|---|
| Salesforce | Recycle Bin — Records are soft-deleted first (unless hard delete is used). | ❌ No — API queries exclude soft-deleted records unless specified. | Use SOQL with ALL ROWS and isDeleted = true. Example: SELECT Id FROM Account WHERE isDeleted=true ALL ROWS | Use Change Data Capture (CDC) for real-time delete detection or poll with ALL ROWS. |
| HubSpot | Archived — Records are soft-deleted into an archived state. | ❌ No — Standard API calls exclude archived records. | Use API with archived=true query param. Example: /crm/v3/objects/companies?archived=true | Use Webhooks for real-time delete events; optionally poll archived records for reconciliation. |
- Salesforce soft deletes are queryable but hidden by default — requires special SOQL syntax to include them. Hard deletes skip Recycle Bin entirely. Can't seem to access via jsforce sobject().find()
- HubSpot archived records are their soft-deleted state — excluded by default but easily included by passing
archived=truein the API. - In both systems, Webhooks (HubSpot) or Change Data Capture (Salesforce) are the best way to track deletions in real time.
CRM types
Complete Field Type Mapping — Salesforce vs HubSpot
| Concept / Data Type | Salesforce Field Type | HubSpot Field Type | Notes / Integration Considerations |
|---|---|---|---|
| Single Line Text | Text | String | Standard text field. Salesforce enforces length limits. |
| Multi-line Text | Text Area (Long), Rich Text Area | String | HubSpot does not differentiate — always string. |
| Number | Number, Currency, Percent | Number (but returned as string via API) | HubSpot API always returns numbers as strings. |
| Boolean | Checkbox (True/False) | Boolean (returned as string "true" / "false") | Cast to Boolean in HubSpot integrations. |
| Date Only | Date | Date | Salesforce stores date without time component. HubSpot stores date as epoch ms (UTC midnight). |
| DateTime | DateTime | DateTime | Salesforce returns ISO 8601 UTC. HubSpot returns epoch ms. |
| Picklist (Single Select) | Picklist | Enumeration (Dropdown Select) | Returns stored value — map to label via property metadata. |
| Picklist (Multi-Select) | Multi-Picklist | Enumeration (Multiple Checkboxes) | Salesforce returns value1;value2;value3 string. HubSpot returns array of strings. |
| String | Both CRMs treat as string — used for matching. | ||
| Phone Number | Phone | String | No enforced formatting — clean client-side. |
| URL | URL | String | No enforced scheme in either — normalize https://. |
| Auto Number | Auto Number | No Native Equivalent | Salesforce auto-incrementing ID. HubSpot lacks this — workarounds required. |
| Formula Field | Formula (Read-Only) | Calculated Property (Enterprise only) | Always read-only via API. Used for computed values. |
| Geolocation | Geolocation (Latitude/Longitude) | Not Supported | Salesforce-only — HubSpot requires custom fields. |
| Lookup / Reference | Lookup(SObject) | Association (via API) | Salesforce has native lookup relationships. HubSpot uses associations API. |
| Owner / User Field | Lookup(User) | HubSpot Owner (User ID) | Always returns internal ID — resolve to Name/Email via API. Cache locally. |
| Address (Compound Field) | Address (Compound Field) | Not Native — Requires separate properties | Salesforce stores address as multiple subfields: Street, City, State, PostalCode, Country. HubSpot stores as separate fields. |
| Record ID | ID (15/18 char) | Internal ID (UUID) | Salesforce has special ID behavior. HubSpot uses UUID strings. |
| Encrypted Text | Encrypted Text | Not Supported | Salesforce-only — API only returns masked value or nothing depending on permissions. |
| Time (Time Only) | Time | Not Supported | Salesforce-only field type for time of day — stored without date. |
| Percent | Percent | Number (returned as string) | Salesforce has dedicated Percent type. HubSpot treats as number (string). |
| Currency | Currency | Number (returned as string) | Salesforce has multi-currency features — may affect display. HubSpot stores as plain number. |
Notes for Integration Teams
Salesforce-Specific Field Types:
- Compound Fields (Address, Name) need to be queried explicitly for parts in API:
SELECT BillingStreet, BillingCity, BillingState, BillingPostalCode, BillingCountry FROM Account
- Auto Number and Encrypted Text are Salesforce-only types.
HubSpot-Specific Behavior:
- No native Address type — all fields like
address,city,state,zipare separate string properties. - No Auto Number — any incremental IDs must be handled externally or via workflows.
- Calculated Properties (Formula equivalent) only available on Enterprise tier.