Skip to content

Business apps: HubSpot and Salesforce

Portward reads business apps the same way it reads files. The gateway, which you run, fetches records through the vendor’s own API into memory, then tokenizes and totals them like any other source. Records never go to disk and never leave your gateway except as answers, under the access rules you set. Both connections are read-only, and the gateway checks this itself:

  • HubSpot: every time the connection opens, and before each hourly reload, the gateway asks HubSpot for the key’s scopes. The gateway refuses a key with any scope that isn’t a read scope. It also refuses a key if HubSpot won’t list its scopes.
  • Salesforce: the gateway reads which objects the integration user could create, change or delete. Those objects are marked writable and can’t be exposed. If every object is writable, the connection is refused. If an exposed object later becomes writable, the gateway stops serving it.
HubSpotSalesforce
Kindhubspotsalesforce
Sign-inService key (or a legacy private app token)External Client App, client credentials flow, run as an integration user
Objects offeredcontacts, companies, deals, owners, plus any CRM object you nameAccount, Contact, Opportunity, plus any queryable object you name (User can be named, but comes back writable)
Hosts contactedapi.hubapi.com onlyyour org’s *.my.salesforce.com only
Records per objectup to 10,000up to 10,000

Both kinds are connected from the setup widget only. There is no config-file form for them.

HubSpot calls private apps “legacy” now and steers single-account integrations to service keys. Portward takes either one. Both are pat-... bearer tokens, and the gateway checks the scopes of both in the same way.

  1. Sign in to the HubSpot account that holds the CRM data, as a Super Admin. A key can only get scopes its creator has.

  2. Go to Development > Keys > Service keys and choose Create service key. Name it Portward (read-only).

  3. Choose Add new scope and tick only these, then Update:

    • crm.objects.contacts.read
    • crm.objects.companies.read
    • crm.objects.deals.read
    • crm.objects.owners.read
    • For any other object you’ll add, its read scope too (for example crm.objects.custom.read for custom objects).

    Tick no write scopes and nothing else (no import, automation, files or tickets). Portward refuses a key that can do more than read.

  4. Review the summary of scopes, choose Create, then on the key’s page Show and Copy. It starts with pat-.

Go to Settings > Integrations > Private Apps (or Development > Legacy apps > Create > Private) and create Portward (read-only) with the same read scopes. Then copy its access token from Auth > Show token.

  1. In Claude, open Portward setup. Choose HubSpot, paste the key, and optionally list more objects (for example tickets, products, or a custom object’s id like 2-1234567). Then connect.
  2. Set each dataset’s rules (for example, tokenize email and phone, totals only for amount) and expose it.

The key sees what its scopes allow, across the whole account. To cut access, delete or rotate the key in HubSpot, then reconnect with the new one. Accounts in any data region work, because api.hubapi.com sends each request to the key’s region.

Salesforce reads run as an integration user that can only read. A client credentials app runs as one user chosen by the admin, so the gateway holds no password and no refresh token.

  1. Setup > Users > New User.
    • License: Salesforce Integration (free, API only; Developer Edition and most editions include some). If you have none left, use Salesforce with a minimal profile.
    • Profile: Minimum Access - API Only Integrations (or Minimum Access - Salesforce).
  2. Setup > Permission Sets > New, named Portward Read Only. If asked, its license is Salesforce API Integration.
    • System Permissions: API Enabled. Nothing else: no View All Data, no Modify All Data.
    • Object Settings, for Account, Contact, Opportunity (and each object you’ll add): Read only. Leave Create, Edit, Delete, View All and Modify All off. Under field permissions, give Read Access to the fields people may see. Fields the user can’t read are never fetched.
    • Any user can read User records, so User needs no object permission. But everyone may edit their own User record, so Salesforce reports User as writable even for a read-only integration user. Portward can’t expose a writable object, so it doesn’t offer User by default. Name it under more objects to see it listed, as writable.
  3. Assign the permission set to the integration user. With the Salesforce Integration license, also assign the Salesforce API Integration permission set license first.

Don’t run the app as an administrator. Every object an admin can change shows up as writable and can’t be exposed.

2. An External Client App with the client credentials flow

Section titled “2. An External Client App with the client credentials flow”
  1. Setup > External Client App Manager > New External Client App. In orgs without it, use Setup > App Manager > New Connected App.
    • Name: Portward. Contact email: yours. Distribution state: Local.
    • Enable OAuth. Callback URL: https://localhost/callback. It’s required but never used.
    • OAuth scopes: Manage user data via APIs (api) only.
    • Turn on Enable Client Credentials Flow. Leave the other flows off.
  2. Open the app’s Policies and edit them:
    • Client Credentials Flow > Run As: the integration user from step 1.
    • Permitted users: Admin approved users are pre-authorized. Add the Portward Read Only permission set (for a Connected App: Manage > Edit Policies, then Manage Permission Sets).
  3. From Settings > OAuth Settings > Consumer Key and Secret (Connected App: Manage Consumer Details), copy the Consumer Key and Consumer Secret.
  4. Find your My Domain under Setup > My Domain. For example acme.my.salesforce.com, or for a Developer Edition acme-dev-ed.develop.my.salesforce.com. A Lightning URL works too. The gateway turns acme.lightning.force.com into acme.my.salesforce.com.

In Claude, open Portward setup. Choose Salesforce, then fill in:

  • My Domain
  • Consumer key
  • Consumer secret
  • Optionally, more objects by API name (for example Case, Project__c)

Then connect. Objects the integration user could change are listed as writable and can’t be exposed. Fix their permissions and reconnect to expose them.

To cut access, deactivate the integration user or revoke the app in Salesforce.

Before it saves, setup checks the key or app and reads what each object offers.

HubSpot:

  1. The key looks like one (“service key: paste the service key (or private app token); it starts with pat-”).
  2. HubSpot knows it (“HubSpot doesn’t recognise this key; check it was copied whole, or rotate it”).
  3. HubSpot says which scopes the key has, and all of them are read scopes. A key HubSpot won’t describe gets “HubSpot didn’t say what the key may do”, and one with any other scope gets “the key can do more than read”.
  4. Each object you named exists (“isn’t a HubSpot object name”).

Salesforce:

  1. My Domain is the org’s own (“domain: the org’s My Domain, like acme.my.salesforce.com (Setup > My Domain)”), and the consumer key and secret are filled in.
  2. Salesforce gives the app an access token for that same org (“Salesforce gave no access token”, “Salesforce named an instance other than the org’s My Domain; it was not used”).
  3. Each object you named can be queried by the integration user (“not objects this user can query”).
  4. Objects the integration user could create, change or delete are listed as writable and can’t be exposed. If every object is writable, setup stops (“this user can change every object offered; run the app as a read-only integration”).

The read-only checks run again before each hourly reload. If an exposed Salesforce object becomes writable later, the gateway stops serving it.

  • Columns come from the vendor’s metadata: HubSpot’s properties API (hidden properties left out) and Salesforce’s describe. Compound address/location fields and binary fields are left out, since they can’t be queried as columns. Each object offers up to 100 columns: the record id, the commonly used fields, then custom fields, then the rest.
    • When a field is added in the app, the dataset is held back until an admin reviews the new column, like any other source.
  • Records are read on the first question, through:
    • HubSpot’s CRM search API (200 per page) and owners API
    • Salesforce’s SOQL query API, following nextRecordsUrl (2,000 per page)
  • Freshness: at most every 5 minutes, a question first fetches the records changed since the newest one the gateway holds, starting 5 minutes earlier to catch late changes. The gateway replaces its copies of those records by id, using HubSpot’s lastmodifieddate / hs_lastmodifieddate and Salesforce’s SystemModstamp. Every hour the gateway reads everything again, so deleted records drop out, and runs the read-only checks first.
  • Caps: the gateway refuses an object with more than 10,000 records rather than answer from part of it, since a partial table would give wrong totals. It also refuses a read that comes back short of the vendor’s own count, or that takes more than 2 minutes. After a failed read, it gives the same error for 30 seconds without calling the vendor again.
  • Rate limits: when a vendor turns a request away for its rate limit (HTTP 429) or a server error (5xx), the gateway waits longer after each try and tries again. When the vendor sends Retry-After, it waits that long. A daily API limit (Salesforce REQUEST_LIMIT_EXCEEDED) is reported to the person asking.
  • Answer records: the record of each answer says when each object’s records were read and how many there were.