Connect HubSpot with Salesforce

Implementation Guide

Overview: Connecting HubSpot and Salesforce

The HubSpot-to-Salesforce integration is arguably the most strategically significant data pipeline in a modern B2B go-to-market stack. HubSpot functions as the marketing automation and inbound demand generation layer—capturing leads through forms, enriching them through behavioral tracking, and scoring and nurturing them through email workflows. Salesforce functions as the sales execution and enterprise system-of-record layer—where qualified leads become Contacts, are associated with Accounts, and progress through Opportunities that feed into revenue forecasting. The boundary between these two systems is the MQL-to-SQL handoff: the precise moment when marketing hands a qualified prospect to a sales representative. When this handoff is manual, it introduces latency, data loss, and attribution gaps. When it is automated and precisely configured, it creates a closed-loop revenue intelligence system where marketing can demonstrate pipeline contribution and sales can act on fully enriched leads without re-entering data.

This integration requires more careful engineering than most two-app syncs because both platforms have overlapping data models (both have Contacts, Companies/Accounts, and Deals/Opportunities), conflicting field naming conventions, different data type expectations, and their own native deduplication logic. Getting it wrong means duplicate records in Salesforce, corrupted lead source attribution, or contact data overwritten in the wrong direction during a bidirectional sync.

Core Prerequisites

On the HubSpot side, you must create a Private App via Settings > Integrations > Private Apps if you are using direct API access, or configure a standard OAuth 2.0 app if you are building a multi-tenant integration. The Private App token (for single-portal use) or OAuth access token must carry the following scopes: crm.objects.contacts.read and crm.objects.contacts.write for reading and creating contact records, crm.objects.companies.read and crm.objects.companies.write for the company/account sync, crm.objects.deals.read and crm.objects.deals.write for deal stage synchronization, crm.schemas.contacts.read to introspect custom contact properties at runtime, and timeline if you intend to write Salesforce activity data back to the HubSpot contact timeline. For webhook-based event reception, navigate to Settings > Integrations > Private Apps > your app > Webhooks and subscribe to contact.propertyChange for the lifecyclestage and hs_lead_status properties.

On the Salesforce side, you require a Connected App with api, refresh_token, and offline_access OAuth 2.0 scopes. The integration service account must have Create and Edit permissions on the Lead, Contact, Account, and Opportunity objects. If your workflow automates lead conversion, the service account also needs the "Convert Leads" profile permission. Critically, if your Salesforce org has Duplicate Management rules active on the Lead or Contact objects, you must review whether those rules are configured to Block or Allow API-sourced inserts—a blocking duplicate rule will silently cause 409 Conflict errors that stall your sync pipeline if not handled explicitly.

Top Enterprise Use Cases

The primary use case is automated MQL-to-SQL handoff. When a HubSpot contact's lifecyclestage property transitions to marketingqualifiedlead—whether by a HubSpot workflow, a manual update, or a lead scoring threshold being crossed—the integration creates a corresponding Lead record in Salesforce, pre-populated with all available demographic and firmographic data plus HubSpot's behavioral context (number of page views, most recent conversion event, lead source). This eliminates the manual BDR process of copying contact information from a HubSpot email notification into Salesforce, which introduces both latency and transcription errors.

The second use case is bidirectional deal and opportunity synchronization for closed-loop attribution reporting. When a Salesforce Lead is converted into a Contact and a new Opportunity is created by a sales representative, the integration should write the Opportunity's details back to HubSpot's Deal object, keeping the HubSpot pipeline current for marketing attribution dashboards. This allows your marketing team to answer the question "which campaigns generated pipeline?" without requiring manual cross-referencing of two separate CRM platforms.

A third use case is company and account synchronization for account-based marketing (ABM) programs. HubSpot's ABM tools require a Company record to exist in HubSpot for account-level contact association. When Salesforce creates a new Account—whether manually by a sales rep or through your CRM enrichment tool—the integration should create or update the corresponding HubSpot Company record, ensuring that ABM target lists and advertising audiences remain synchronized.

Step-by-Step Implementation Guide

The direct API integration approach begins with subscribing to HubSpot's CRM webhook endpoint. Configure your webhook subscription to listen for contact.propertyChange events on the lifecyclestage property. When a contact reaches marketingqualifiedlead status, HubSpot delivers a POST request to your configured listener endpoint with a payload structured as follows:

{
  "eventId": 100034,
  "subscriptionId": 882341,
  "portalId": 4556789,
  "occurredAt": 1700123456789,
  "subscriptionType": "contact.propertyChange",
  "attemptNumber": 0,
  "objectId": 5501,
  "propertyName": "lifecyclestage",
  "propertyValue": "marketingqualifiedlead",
  "changeSource": "WORKFLOWS"
}

Note that this webhook payload contains only the contact's HubSpot ID and the property that changed—it does not include the full contact record. Your middleware must therefore issue a secondary enrichment call: GET https://api.hubapi.com/crm/v3/objects/contacts/5501?properties=firstname,lastname,email,company,jobtitle,phone,city,state,country,hs_analytics_source,hs_analytics_source_data_1,hubscore using the objectId from the webhook. With the full contact record retrieved, your middleware must then check Salesforce for a pre-existing Lead or Contact with a matching email address to prevent duplicate creation. Issue a Salesforce SOQL query: SELECT Id, Email, IsConverted FROM Lead WHERE Email = '[email protected]' AND IsConverted = false LIMIT 1. If no match is found, POST the new Lead to /services/data/v58.0/sobjects/Lead/. If a match exists, PATCH the existing record. The request body for Lead creation should follow this structure:

{
  "FirstName": "Jane",
  "LastName": "Doe",
  "Email": "[email protected]",
  "Company": "Acme Corp",
  "Title": "VP Engineering",
  "Phone": "+14155550100",
  "LeadSource": "Inbound - Organic Search",
  "Lead_Source_Detail__c": "hs_organic_search",
  "HubSpot_Contact_ID__c": "5501",
  "HubSpot_Lead_Score__c": 87,
  "Status": "MQL - Pending Review"
}

The HubSpot_Contact_ID__c field is a custom external ID field you must create in Salesforce beforehand. Storing it enables idempotent upsert operations using PATCH /services/data/v58.0/sobjects/Lead/HubSpot_Contact_ID__c/5501, which is safer than a GET-then-POST pattern because it is atomic and eliminates the race condition where two concurrent webhook deliveries both fail the duplicate check and create two Lead records.

For Zapier, configure the HubSpot "Contact Property Change" trigger monitoring the Lifecycle Stage property. Add a Filter step that only continues when the lifecycle stage value equals "marketingqualifiedlead" (Zapier normalizes HubSpot's internal enum values, so verify the exact string in the trigger step's output). Add a Salesforce "Find Record" step querying Leads by Email to check for duplicates. Use a Zapier Paths step to branch: one path handles existing records with a "Update Record" action, the other handles new records with a "Create Record" action. Map the HubSpot trigger's {{First Name}}, {{Last Name}}, {{Email}}, {{Company Name}}, and {{Job Title}} fields to the corresponding Salesforce Lead fields in each path.

For Make, the scenario begins with a HubSpot "Watch Contact Property Changes" module. Because Make uses polling rather than true webhooks for this module, pair it with a HubSpot "Get a Contact" module chained immediately after to retrieve the full property set before passing data downstream. Use a Salesforce "Search Records" module with a SOQL condition matching the contact's email. A Router module splits the flow based on whether the search returned any results. The "record exists" route uses a Salesforce "Update a Record" module; the "no record" route uses a Salesforce "Create a Record" module with explicit field mappings defined in the module configuration panel.

Common Pitfalls & Troubleshooting

A 401 Unauthorized response from HubSpot's API most commonly signals that a Private App token has been manually rotated in HubSpot's settings or that the token is being transmitted incorrectly. HubSpot Private App tokens must be sent in the Authorization: Bearer {token} request header. Unlike OAuth access tokens, Private App tokens do not expire on a schedule, but they are immediately invalidated if the Private App is deleted and recreated or if a HubSpot super admin rotates the token. Implement monitoring on your integration's HubSpot API calls and alert on sustained 401 rates above zero, as a single 401 from a Private App token is a strong signal of a token rotation event rather than a transient network error.

A 409 Conflict from Salesforce when attempting to create a Lead record almost always indicates that a Salesforce Duplicate Management rule is configured to block the insert. Inspect the response body, which will contain a duplicateResult object identifying the matching existing records. The appropriate resolution depends on your business logic: if the matching record is an already-converted Lead (i.e., the contact exists as a Salesforce Contact), update the Contact record instead of attempting to create a new Lead. If the matching record is an unconverted Lead, patch it with any new HubSpot property values.

A 422 Unprocessable Entity from either platform indicates a field-level validation failure. The response body's message array will identify the offending field and the constraint that was violated. The most common causes are: sending a HubSpot Unix millisecond timestamp for a date property that Salesforce expects as an ISO 8601 string (2025-09-30 not 1727654400000), sending a picklist value that does not exist in the target field's defined value set (e.g., a custom Lead Status value in HubSpot that has no corresponding entry in Salesforce's Status field picklist), or exceeding a field's maximum character length. Maintain a field-type conversion map in your middleware configuration and validate all payloads against it before dispatch.

Field mapping drift is a long-term operational risk unique to this integration. As your marketing team adds custom contact properties in HubSpot and your RevOps team adds custom fields in Salesforce, the mapping between the two must be actively maintained. Establish a governance process where new custom fields in either system trigger a review of the integration's field mapping configuration. Automated schema introspection—calling HubSpot's /crm/v3/properties/contacts endpoint and Salesforce's /services/data/v58.0/sobjects/Lead/describe/ endpoint on a scheduled basis to detect new or changed fields—can surface mapping gaps before they cause data loss.