Back to Blog
Salesforce

How to Create a HubSpot Private App or OAuth App for a Salesforce Integration

TS

Tushar Sharma

Chief Executive Officer

10 min read - Jul 18, 2026

Why the HubSpot Side Comes First

Every HubSpot to Salesforce integration starts in the same place, and it is not Salesforce. Before an Apex callout can create a contact or a Flow can pull deal data, HubSpot needs to know exactly who is calling its APIs and what they are allowed to touch. Picking the wrong auth model costs days: HubSpot has no client_credentials grant, and its legacy public app builder was switched off in June 2026.

This is the zoomed-in first step of our full technical guide at cloudsheer.com/integrations/hubspot/technical-guide. Cloudsheer has shipped this integration across 8 client projects, and the sequence below is what we run on day one: create the app, pick scopes, copy the token, wire it into a Salesforce External Credential, and fire one smoke-test callout before writing any real code.

What You Need Before You Start

  • Super Admin access on the target HubSpot portal. Private apps cannot be created or viewed without it; accounts are capped at 20 private apps.
  • A Salesforce org with API access (Enterprise, Unlimited, Performance, or Developer Edition) and rights to edit Named Credentials and permission sets in Setup.
  • A written list of the objects you will read and write (contacts, companies, deals, owners); it becomes your scope list in Step 2.
  • A password manager entry for the token, a production password from the second it exists: no email, no Slack, no spreadsheet.
  • For the OAuth route only: a separate app developer account at developers.hubspot.com. Private apps do not appear there, which is where most people get lost.
  • Your portal's region. Tokens are prefixed pat-na1- (North America) or pat-eu1- (EU); both call https://api.hubapi.com.

Step 1: Choose Private App, Public OAuth App, or the Native Sync

A private app issues one static bearer token scoped to one portal: no redirect URIs, no refresh logic, no expiry timer. It is the only fully headless server-to-server option, and our default on single-portal builds.

A public OAuth app is for multi-portal products: customers authorize it, and you exchange a Client ID and Client Secret for 30-minute access tokens plus a long-lived refresh token. Since June 23, 2026 new public apps must be built on the projects-based platform via the HubSpot CLI (hs project create, config in app-hsmeta.json); existing legacy public apps keep working, with credentials in the app's Auth settings tab.

Neither is the native HubSpot Salesforce sync you install via the Marketplace icon in HubSpot's top nav (search Salesforce, click Install, approve the AppExchange managed package). That is a no-code field sync requiring HubSpot Professional or Enterprise; it gives you zero API callout plumbing.

Step 2: Create the Private App and Pick Scopes

  • Log in at app.hubspot.com as a Super Admin, click the settings gear icon in the top nav, then open Integrations > Private Apps in the left sidebar and click Create a private app. Current docs call the same screen Development > Legacy apps > Create legacy app > Private; both land in the same builder.
  • On the Basic Info tab, use a name a future admin can decode, for example Salesforce Integration - Production. Logo and description are optional.
  • On the Scopes tab, click + Add new scope and use the search box. For a standard CRM sync tick crm.objects.contacts.read, crm.objects.contacts.write, the matching companies and deals pairs, and crm.objects.owners.read so you can resolve HubSpot owner fields to real users.
  • Watch the tier gates: crm.objects.leads.read and .write need Sales Hub Professional or Enterprise; crm.objects.custom.read and .write need Enterprise. On lower tiers the API answers MISSING_SCOPES even with the box ticked.
  • Review the Summary of selected scopes, skip the Webhooks tab unless you need event pushes, click Create app in the top right, and confirm in the dialog.

Step 3: Copy the Token and Plan Its Rotation

  • Open the app, open the Auth tab, click Show token, then Copy. It starts with pat-na1- or pat-eu1-; store it immediately.
  • Despite what many guides claim, this token is not shown once; Show token reveals it any time a Super Admin returns. The genuinely one-time values are the OAuth authorization code and the refresh token.
  • Send it as the header Authorization: Bearer pat-na1-... against https://api.hubapi.com. Never in a URL: hapikey query keys were sunset on November 30, 2022.
  • Learn the two rotation buttons now. Rotate and expire now kills the old token instantly, and every Salesforce callout 401s until you paste the new value into the External Credential. Rotate and expire later gives a 7-day overlap, the zero-downtime option.

Step 4: The Public OAuth App Route, Only If You Need Multi-Portal

  • Register at least one Redirect URL in the app's Auth settings. HubSpot's rule: https only in production (http only for localhost), a real domain, never an IP. Your redirect_uri must match a registered URL character for character, trailing slash included.
  • Send the user to https://app.hubspot.com/oauth/authorize?client_id=...&scope=oauth%20crm.objects.contacts.read&redirect_uri=..., scopes space separated, URL encoded as %20. The user picks a portal, clicks to connect, and a single-use code returns to your redirect_uri.
  • Exchange it: POST https://api.hubapi.com/oauth/v1/token with grant_type=authorization_code, client_id, client_secret, redirect_uri, and code. Persist the refresh_token immediately; it is only returned here. Access tokens expire in 30 minutes (expires_in: 1800).
  • The v1 OAuth endpoints sunset on February 16, 2027; new work should target POST /oauth/2026-03/token and its introspect and revoke siblings.
  • Scope edits are not free: existing installs must re-authorize, and the change can invalidate outstanding refresh tokens.

Step 5: Wire the Token into a Salesforce External Credential

  • In Setup, type Named Credentials into Quick Find, open the External Credentials tab, and click New. Label and Name: HubSpot. Authentication Protocol: Custom. Save.
  • Under Principals, click New. Identity Type: Named Principal, Sequence Number: 1. Add an Authentication Parameter named HubSpotToken with your pat-na1- token as the Value, then Save. Salesforce encrypts and masks it; you can overwrite it but never read it back.
  • Under Custom Headers, click New. Name: Authorization. Value: {!'Bearer ' & $Credential.HubSpot.HubSpotToken}. The syntax is $Credential.[ExternalCredentialName].[ParameterName].

Step 6: Named Credential, Two Checkboxes, and the Permission Wall

  • On the Named Credentials tab click New. Label and Name: HubSpot_API. URL: https://api.hubapi.com. External Credential: HubSpot.
  • The trap that costs the most debugging hours: uncheck Generate Authorization Header and check Allow Formulas in HTTP Header. Get either wrong and Salesforce suppresses or clobbers your custom Authorization header, and the callout dies with a clean-looking 401.
  • Grant access, because the credential alone does nothing: in your integration permission set, add the HubSpot principal under External Credential Principal Access and confirm object permissions on User External Credentials. Skip either and callouts fail at runtime.
  • From Apex or Flow, call callout:HubSpot_API/crm/v3/objects/contacts. No Remote Site Setting is needed; Named Credentials bypass that layer.

Step 7: OAuth Variant, Auth Provider Plus Browser Flow

  • Public app route only. In Setup open Auth. Providers, click New, Provider Type: Open ID Connect. Consumer Key: HubSpot Client ID. Consumer Secret: Client Secret. Authorize Endpoint URL: https://app.hubspot.com/oauth/authorize. Token Endpoint URL: https://api.hubapi.com/oauth/v1/token. Default Scopes: space separated, for example oauth crm.objects.contacts.read crm.objects.contacts.write. Save.
  • Copy the generated Callback URL (https://<MyDomain>.my.salesforce.com/services/authcallback/<URLSuffix>) and paste it into the HubSpot app's Redirect URLs. The exact-match rule from Step 4 applies.
  • Create an External Credential: Authentication Protocol OAuth 2.0, Authentication Flow Type Browser Flow, Authentication Provider the one you just made. Add a Named Principal (Sequence 1) and click Authenticate; a browser window opens HubSpot's consent screen, pick the portal, click to connect.
  • Salesforce then stores the refresh token and mints fresh 30-minute access tokens automatically, the same recipe it documents for its Data Cloud HubSpot connector. There is no headless option: an admin clicks Authenticate once per principal, per org.

Step 8: Smoke Test with a Real Endpoint

Do not write the integration first and debug auth later. As a user holding the Step 6 permission set, open the Developer Console and run: HttpRequest req = new HttpRequest(); req.setEndpoint('callout:HubSpot_API/crm/v3/objects/contacts?limit=1'); req.setMethod('GET'); HttpResponse res = new Http().send(req); System.debug(res.getStatusCode() + ' ' + res.getBody());

A healthy setup logs 200 and a results array with one contact and its createdate, email, firstname, and lastname properties. A 401 points at Steps 5 and 6, a 403 MISSING_SCOPES at the Scopes tab, a Remote Site Settings exception at a hardcoded URL. Note the X-HubSpot-RateLimit-Remaining header too: a Professional portal's private apps get 190 requests per 10 seconds and 625,000 calls per day account-wide.

Common Errors and How to Fix Them

  • 401 EXPIRED_AUTHENTICATION (The OAuth token used to make this call expired 5 minute(s) ago): the access token outlived its 30-minute TTL. Refresh proactively; the Browser Flow credential in Step 7 does it for you.
  • 401 INVALID_AUTHENTICATION (Authentication credentials not found): missing or malformed Authorization header. Check for a typo in Bearer, whitespace pasted with the token, or Generate Authorization Header still checked.
  • 403 MISSING_SCOPES (This app hasn't been granted all required scopes): add the listed scope on the Scopes tab, or for a public app add it and have the customer re-authorize. The identical 403 fires when the tier lacks the feature, like Leads below Sales Hub Professional, so check tier first.
  • 400 BAD_REFRESH_TOKEN (missing or invalid refresh token): the portal uninstalled the app, the token was revoked, or scopes changed. The only fix is rerunning the OAuth flow; in Salesforce, click Authenticate on the principal again.
  • Couldn't complete the connection: The redirect_uri does not match the client registered redirect_uri: exact-match failure. Compare http versus https, trailing slash, subdomain, and register the Salesforce Callback URL verbatim.
  • System.CalloutException Unauthorized endpoint, please check Setup->Security->Remote site settings: you called https://api.hubapi.com directly. Switch to callout:HubSpot_API; Named Credentials bypass Remote Site Settings.
  • Callout runs but HubSpot sees no Authorization header: the running user lacks External Credential Principal Access or permissions on User External Credentials. Fix the permission set, not the credential.
  • 429 You have reached your ten_secondly_rolling limit: back off using X-HubSpot-RateLimit-Remaining and X-HubSpot-RateLimit-Interval-Milliseconds. The CRM Search API throttles at 5 requests per second account-wide and sends no rate-limit headers at all.

Security Checklist Before You Ship

  • The token exists in exactly two places: the External Credential parameter (encrypted, write-only) and your password manager. Never mirror it into custom settings, custom metadata, or Apex source.
  • Scopes are least-privilege. If the integration never edits contacts, drop crm.objects.contacts.write; private app scope edits apply to the same token instantly, so tightening costs nothing.
  • Rotation is calendared every 6 months, always via Rotate and expire later so the 7-day overlap covers updating the Salesforce principal with zero downtime.
  • Principal access lives in a dedicated integration permission set, so you can list exactly who can fire HubSpot callouts.
  • Public app redirect URLs are https, domain-based, and pruned of dead environments.
  • Sandbox and production use separate private apps and tokens, so a sandbox leak never reaches production data.
  • Anything calling the v1 OAuth endpoints has a migration ticket dated before February 16, 2027.

Where This Fits in the Full HubSpot Build

Credentials are step one of a much longer build. The full technical guide at cloudsheer.com/integrations/hubspot/technical-guide picks up exactly here: mapping HubSpot objects onto Salesforce leads, accounts, and opportunities, sync direction and conflict rules, queueable Apex callout patterns, webhook ingestion, and budgeting the daily API ceiling so a backfill does not starve the nightly sync.

If you build with Claude Code, our free skill scaffolds this whole setup with the traps already coded around: npx skills add shivamgoel-cloudsheer/Claude-skills --skill hubspot-salesforce.

How Cloudsheer Can Help

Cloudsheer has shipped the HubSpot to Salesforce integration across 8 client projects: single-portal private-app builds, multi-portal OAuth products, and hybrids running the native sync app alongside custom API work. Every error above is one we have fixed in a production org, which is why the fixes name exact checkboxes instead of telling you to check your configuration.

If you would rather hand this off, we run the whole arc: auth architecture, scope audits, the Salesforce wiring, rotation runbooks your admins can follow at 2 a.m., and the sync logic on top. Book a free discovery call at cal.com/cloudsheer-consulting/30min

Want to see how this applies to your business?

Book a free 30-minute call. We will walk through your specific use case and show you what's possible.

Book Free Discovery Call
Ask me anything