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 access | Direction | Scope |
|---|---|---|
| Contacts | Read + write | crm.objects.contacts.read, crm.objects.contacts.write |
| Contact properties (field discovery, and the mapping screen) | Read | crm.objects.contacts.read (also satisfied by crm.schemas.contacts.read) |
| Deals — read-only mirror, never written | Read | crm.objects.deals.read |
| Deal pipelines & stages (stage labels) | Read | crm.objects.deals.read |
| Deal ↔ contact associations | Read | Covered 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) | Read | crm.objects.owners.read |
| Marketing lists + list memberships | Read | crm.lists.read |
| Account info (portal ID, used to build links back to HubSpot) | Read | Not independently verified — see note below |
Notes:
crm.lists.readis 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
oauthscope, 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) | Direction | Privilege needed |
|---|---|---|
| Contact | Read + write | Read, Write |
| Lead | Read + write | Read, Write |
| Opportunity — read-only mirror, never written | Read | Read |
| List (marketing lists) | Read | Read |
List Member (listmembers) | Read | Read |
Entity/attribute metadata (EntityDefinitions) | Read | Read 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)
If the connection fails
The error names which of the four fields to look at. The two halves mean different things:
- "Azure rejected…" — Azure AD would not issue a token, so Dynamics was
never contacted. The message says which field: the client secret (copy the
secret's Value, not its Secret ID — and check it hasn't expired), the
Application (client) ID (not the Object ID), the Azure tenant ID, or an
Organization URL that must be
https://yourorg.crm.dynamics.comwith no path. Each message carries anAADSTS…code your Azure administrator can look up. - "Dynamics rejected the sign-in" — your Azure credentials are fine and the token was issued, but Dataverse refused it. Almost always this means the environment has no Application User for that Client ID, or it has one with no security role. Add it as described above.
A wrong Organization URL does not usually produce the first error: Azure
issues a token for any *.crm.dynamics.com address, so a typo there surfaces
later as a failure to reach the environment.
Data push (optional — only if you enable it)
Pushing engagement data into your environment needs two things beyond the table above. See Pushing engagement data into Dynamics 365 for the full story.
| What | When | Privilege |
|---|---|---|
| Creating our publisher, solution and tables | One-off, when you press Provision | Customisation rights — the System Customizer role is the simple way. You can remove it again afterwards: once provisioning succeeds we only ever write rows, never schema |
| Writing rows into those tables | Ongoing | Create + Write on the tables we created (covered by the role your connection already uses for Contact/Lead) |
| Your users seeing those rows | Ongoing | Organization-level Read on the created tables, in whichever role your users have. The tables are user-owned and the rows are owned by the Application User, so without this the data is present but invisible — an empty grid, no error. This is the most common way data push appears broken while working correctly |
We never delete rows in your environment, never modify a table we did not create, and never add anything to a Microsoft solution.
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 connects by authorising our app — you click Connect with Pipedrive, sign in to Pipedrive in the window that opens, and approve the access listed on Pipedrive's own consent screen. There is no API token to generate and nothing to paste. We never ask for, see, or store your Pipedrive password or an API token.
Two things follow from that, and they're the ones worth understanding:
- The connection acts as the person who authorised it. Whoever clicks through the consent screen is the Pipedrive user the connection behaves as. The permissions below are the ceiling; that user's own access in Pipedrive is the floor, and the lower of the two wins. Connecting as a user with restricted visibility gives you a connection with restricted visibility. Prefer an account that can see everything you want synced, and ideally not one that's about to leave — if their Pipedrive access is removed, the connection stops working.
- It's all or nothing. Pipedrive shows the whole list on one screen and you either approve it or cancel — there are no individual tick boxes, so there's no way to end up half-granted. If you cancel, nothing is connected and nothing is stored.
What we ask for, and why
| What we access | Direction | Permission requested |
|---|---|---|
| Persons (contacts) | Read + write | contacts:full |
| Person field definitions (field discovery, and the mapping screen) | Read | contacts:full |
| Finding a person by email or phone (matching an outbound change to the right record) | Read | search:read |
| Leads | Read + write | leads:full |
| Deals — read-only mirror, never written | Read | deals:read |
| Deal participants (contact associations on a deal) | Read | deals:read |
| Deal stages (stage labels) | Read | deals:read |
| Saved filters (Pipedrive's filters stand in for marketing lists) | Read | Covered by the person and lead permissions above |
Account basics and the connection health check (users/me) | Read | base |
| Owner names (turning the owner id on a record into a person's name) | Read | users:read |
| Change notifications for persons, so we don't have to keep asking | Read + write | webhooks:full |
Notes:
- Nothing here writes to your Pipedrive settings or schema. We don't ask for admin access. When a mapped dropdown gains a value we haven't seen before, the new option is created on our side, never in your Pipedrive field definitions.
- Deals are a one-way mirror. We read them; we never create, edit or delete a deal, which is why the deal permission is read-only.
- Webhooks are a quota measure, not extra reach. Pipedrive meters API usage against a daily budget, and having your account tell us what changed costs far less of it than asking repeatedly. It doesn't give us access to anything the read permissions above don't already cover.
users:readis only so an owner has a name. A change notification identifies a record's owner as a bare numeric id and carries no name anywhere, so without the user directory the owner on a contact would show as a raw Pipedrive user id rather than a person.basecoversusers/me— the account you connected as — and nothing else, so the directory is a separate permission. We read it once per batch of changes, and only to turn ids into names.- Notifications cover persons only. A person edited in Pipedrive reaches us within seconds, and we then re-check persons on a slower schedule purely to catch anything a dropped notification missed. Leads and deals are not notified — a lead notification carries no contact details and a deal notification carries neither its stage nor who it's linked to, so acting on either would create duplicates — and they keep being checked on the regular sync schedule instead. Nothing you change is missed either way; leads and deals simply arrive on that schedule rather than immediately.
If something looks incomplete
- The list picker under "Limit inbound sync" is empty even though your Pipedrive account has saved filters: this isn't a permission you can grant separately. Disconnect and connect again; if it's still empty, that's worth reporting to us rather than changing anything in Pipedrive.
- Nothing syncs at all, or the connection reports an error immediately: check that the person who authorised the connection still has access to Pipedrive, and that API access is enabled for their permission set (Settings → Company settings → Permission sets → your set → "Use API"). Pipedrive treats that as an on/off switch independent of the object-level permissions a user has in the app.
- You want to hand the connection to someone else, or the authorising user is leaving: disconnect and reconnect as the new person. There's no way to re-point an existing connection at a different Pipedrive user.
Removing our access
Disconnecting the CRM in Settings → Integrations revokes our access at Pipedrive and uninstalls the app from your Pipedrive account — you don't need to do anything on the Pipedrive side as well. Contacts already synced into the app stay; segments built from Pipedrive saved filters stop working until you reconnect.