CRM permissions reference

Each CRM provider gates API access a different way, and a missing grant usually fails silently rather than with an error you'd notice — the symptom is an empty list picker, a field that never shows up to map, or a sync that quietly imports nothing, not a red banner telling you what's wrong. If something in your CRM connection looks incomplete, this is the first place to check.

This page lists what we actually call, grouped by provider, so you (or whoever administers your CRM) can grant exactly what's needed — no more, no less.

HubSpot

HubSpot connects with a Private App access token. You create the private app in HubSpot yourself (Settings → Integrations → Private Apps), tick the scopes below by hand, and paste the token into our Connect dialog. There's no prompt or consent screen on our side — whatever scopes you didn't tick, we simply can't use, and the failure is silent (see the table below for which of these fail loudly and which don't).

What we accessDirectionScope
ContactsRead + writecrm.objects.contacts.read, crm.objects.contacts.write
Contact properties (field discovery, and the mapping screen)Readcrm.objects.contacts.read (also satisfied by crm.schemas.contacts.read)
Deals — read-only mirror, never writtenReadcrm.objects.deals.read
Deal pipelines & stages (stage labels)Readcrm.objects.deals.read
Deal ↔ contact associationsReadCovered by crm.objects.deals.read + crm.objects.contacts.read — there's no separate associations scope
Owners (to show a display name on an owner field)Readcrm.objects.owners.read
Marketing lists + list membershipsReadcrm.lists.read
Account info (portal ID, used to build links back to HubSpot)ReadNot independently verified — see note below

Notes:

  • crm.lists.read is not what powers "Limit inbound sync." HubSpot's search API can't filter by list membership at all, so the inbound scope feature uses a boolean contact property instead (see CRM sync scope). This scope is for Connected segments — building a segment from a HubSpot list — a separate feature. Miss it and that list picker is simply empty, with nothing telling you why.
  • We couldn't pin down from our own code exactly which scope (if any) private apps need for the account-info lookup that resolves your portal ID — some HubSpot documentation lists it under the base oauth scope, which private apps may not expose as a checkbox at all. If your portal ID isn't resolving, check your private app's granted scopes in HubSpot directly.

Dynamics 365 (Dataverse)

Dynamics authenticates with OAuth client-credentials against {resource}/.default — that grants an access token for the whole organization, not a specific set of API scopes. What the connection can actually do is entirely controlled by the Dataverse security role assigned to the Application User created for this connection (Power Platform admin center → your environment → Application users). Give that role too little and the same thing happens as with HubSpot: no error, just an empty picker or a field that never resolves.

Table (entity)DirectionPrivilege needed
ContactRead + writeRead, Write
LeadRead + writeRead, Write
Opportunity — read-only mirror, never writtenReadRead
List (marketing lists)ReadRead
List Member (listmembers)ReadRead
Entity/attribute metadata (EntityDefinitions)ReadRead access to entity metadata — this isn't a record-level privilege in the same sense as the rows above; if your security role can query the API at all it typically already has this

Also read, only to enrich the read-only deal mirror (currency code and contact associations on an opportunity) — missing these degrades that one field gracefully (logged, left blank) rather than failing the sync:

  • Connections — read (resolves opportunity stakeholders/contacts)
  • Account — read (resolves an opportunity's primary contact when the deal's customer is an account rather than a contact directly)
  • Transaction Currency — read (resolves the ISO currency code on a deal)

Dynamic marketing lists are excluded on purpose, not by a missing privilege. Even with full Read on List, a dynamic list's membership is a saved query with no member rows — there's nothing for our filter to traverse — so it shows in the scope picker greyed out rather than being usable. See CRM sync scope for what this looks like.

Pipedrive

Pipedrive is different again: the connection uses a personal API token, and a token carries exactly the permissions of the Pipedrive user it belongs to — there's no separate scope list to grant. Whatever that user can see and do in the Pipedrive UI, the connection can see and do through the API. Nothing more.

To make sure the connection works, the user whose token you use needs:

What we accessDirectionWhat the token owner needs in Pipedrive
Persons (contacts)Read + writeView and edit access to Contacts
LeadsRead + writeView and edit access to Leads
Deals — read-only mirror, never writtenReadView access to Deals
Deal participants (contact associations on a deal)ReadView access to Deals (same permission as above)
Deal stagesReadView access to Deals
Filters (marketing lists — Pipedrive's filters stand in for lists)ReadView access to Filters/saved views for People and Leads
Person / lead field definitions (field discovery, mapping screen)ReadView access to Contacts and Leads (same permission as above)
users/me (connection health check)ReadAny authenticated user — no extra permission

And separately, at the account level: API access must be enabled for that user's permission set (Settings → Company settings → Permission sets → your set → "Use API"). Pipedrive treats this as an on/off switch independent of the object-level permissions above — a user can have full access to everything in the app and still be unable to generate an API token if this isn't turned on.