---
name: microsoft-dynamics-oauth-app
description: >-
  Registers a Microsoft Entra ID application for Dynamics 365 / Microsoft
  Dataverse access. Builds on the `microsoft-entra-app-registration` base
  skill; read that first. This skill covers only what Dynamics adds: the
  per-customer, per-environment Dataverse resource URL and the Global
  Discovery Service, the `user_impersonation` / `.default` scopes, the
  application user plus security role a customer admin must set up before an
  app-only token works, Dataverse row-level security, the product-family split
  (Sales, Customer Service, Business Central, Finance & Operations, GP), Web
  API versioning and service protection limits. Use when asked to get Dynamics
  365 OAuth credentials, connect Dataverse, Sales or Customer Service, fix a
  Dynamics connector returning 401 or 403 with a valid token, set up Business
  Central or Finance & Operations API access, or find the environment URL a
  connector should call.
---

# Dynamics 365 / Dataverse (Microsoft Entra ID) OAuth2 App Registration

Get a working OAuth2 client for Dynamics 365 — an Entra ID app registration whose tokens are accepted by a
*specific customer's* Dataverse environment.

Registering the app is the base skill's job. Two things make Dynamics different from every other Entra product, and
both of them fail *after* the registration looks perfect. First, there is no single API host: the resource you
request a token for is the customer's own environment URL, so the connector has to discover it per customer (§2).
Second, for app-only access an Entra app registration is **not enough** — a customer admin must create an
**application user** in the environment and give it a **security role**, or a perfectly valid token gets 401/403
forever (§3). Everything else here is the product-family map (§1), which decides whether any of this applies at all.

## Built on: `microsoft-entra-app-registration`

**Read the base skill first.** It is the source of truth for the Entra mechanics this one assumes:

- **Supported account types** — single tenant vs multitenant vs personal Microsoft accounts, and `AADSTS50194` when
  a single-tenant registration is called through `/common`. Dynamics is work/school only, so pick **Multiple Entra ID
  tenants**; personal-account audiences buy nothing here and cost permission ceiling.
- **Redirect URIs** — the Web vs SPA vs public-client platform choice (only **Web** may present a client secret),
  exact case-sensitive matching, the count and length limits, and the Unified.to callback hosts.
- **Delegated vs application permissions, and admin consent** — how the two modes differ, which permissions need an
  administrator, and how to send a customer admin an admin-consent URL rather than instructions.
- **Client secrets and certificates** — the 24-month cap with no never-expires option, the **Value** shown exactly
  once, `AADSTS7000222` when it lapses, add-then-delete rotation, and certificates as the recommended alternative.
- **`offline_access`, publisher verification, and the generic AADSTS symptom table** — a Dataverse scope carries no
  refresh token of its own; `offline_access` is what gets you one, and risk-based step-up consent can block ordinary
  users from consenting to an unverified multitenant app at all.

The base also holds the inputs to collect, the run order, tenant ownership, the credential hand-off and the generic
stop-and-ask conditions. Without it you are missing all of that — everything below assumes it and covers only what
Dynamics adds.

**Three places the base needs reading with a Dynamics correction:**

1. The base says Microsoft Graph **application** permissions require a **Privileged Role Administrator**. Dataverse
   and Business Central app permissions are on their *own* APIs, not Graph — a Cloud Application Administrator or
   Application Administrator can consent those. Do not tell a customer they need a Privileged Role Administrator for
   a Dataverse-only app; you will cost them a scheduling round trip for nothing.
2. The base's per-cloud table gives authority and **Graph** hosts. Dataverse has its own per-cloud hosts (§2) and the
   Graph host is irrelevant to it.
3. The base assumes the resource you request a token for is a fixed, known host. For Dataverse it is the customer's
   environment URL, which you do not know at registration time (§2).

## 1. Which Dynamics is this? The product family is not one API

"Dynamics 365" is a brand over several unrelated APIs. Establish which one before anything else — the answer changes
the host, the scope, the admin step and the throttling model.

| Product | Data platform | API surface | Token resource |
| --- | --- | --- | --- |
| **Sales** | Dataverse | Dataverse Web API, `/api/data/v9.x` | The environment URL |
| **Customer Service / Customer Engagement** | Dataverse | Dataverse Web API | The environment URL |
| **Field Service, Project Operations** | Dataverse | Dataverse Web API | The environment URL |
| **Contact Center** | Dataverse (Customer Service lineage) | Dataverse Web API | The environment URL |
| **Customer Insights – Journeys** (was Marketing) | Dataverse | Dataverse Web API, `msdyncrm_*` tables | The environment URL |
| **Customer Insights – Data** | Its own store | Its own REST API **plus a subscription key**; Microsoft now recommends reading the same tables through Dataverse instead | A different Entra API |
| **Business Central** | Its own (BC) database | `api.businesscentral.dynamics.com/v2.0/{environment}/api/v2.0` | One fixed host (§5) |
| **Finance & Operations** (Finance, Supply Chain, Commerce, HR) | Its own (AOS) | OData at `{environment}/data` | The environment root URL (§5) |
| **Dynamics GP / SL** | On-premises SQL | Web services the customer hosts; no Entra OAuth | Not applicable (§5) |

**This skill covers the Dataverse-backed products** — Sales, Customer Service / Customer Engagement, Contact Center,
Customer Insights – Journeys and the rest of the first block. §5 covers what changes for Business Central, Finance &
Operations, Customer Insights – Data and GP, which are genuinely different auth surfaces and not this file's subject.

One consequence worth stating early: **all the Dataverse products in an environment share one Dataverse**. A token
scoped to `https://{org}.crm{n}.dynamics.com` reaches Sales tables, Customer Service tables and Journeys tables
alike, subject to the security role. There is no per-app scope separating them. If a customer expects "read-only
access to Sales and nothing else", that separation comes from the **security role** (§3), not from OAuth.

## Extra inputs to collect

On top of the base skill's batch:

| Input | Notes |
| --- | --- |
| **Which Dynamics product** | Decides whether this file applies at all (§1) |
| **Delegated (a user authorizes) or app-only (client credentials)?** | App-only adds a per-customer admin step that cannot be self-served (§3) |
| **The customer's environment URL**, or agreement to discover it | Per customer, per environment — never a constant (§2) |
| **If app-only: who creates the application user, and which security role?** | Needs a Power Platform / environment admin per customer (§3) |
| **Which cloud** | Commercial, GCC, GCC High, DoD, China — different Dataverse *and* discovery hosts (§2) |
| **Does the customer have more than one environment?** | Production/sandbox/UAT each have their own URL and their own application user |

## Platform state (verified 2026-09-20 — re-verify before trusting)

- **The Dataverse Web API is at `{environment-url}/api/data/v9.2/`.** Microsoft documents v9.0, v9.1 and v9.2 as
  having **identical behavior** with no breaking changes between them, and the guidance is to pin the version that
  was current when the code was written rather than follow the newest. v9.2 is current. Older `v8.x` is a separate,
  frozen surface with documented behavioral differences (FetchXML character encoding, duplicated table/column names).
- **The scope is resource-scoped, and Microsoft's own wording is precise:** `"<environment-url>/user_impersonation"`
  for a public client, `"<environment-url>/.default"` for a confidential client. Both are documented on the same
  page; neither is a Graph scope, and neither works against a host other than that customer's environment.
- **The delegated permission in the Entra portal is named "Access Dynamics 365 as organization users"**, under the
  **Dynamics CRM** API (older docs say "Dynamics 365", and older-still docs say "Dynamics CRM Online"). It is one
  permission; there is no read-only variant. Least privilege is expressed in the Dataverse security role, not here.
- **Application-user management lives in the Power Platform admin center** — *Manage → Environments → (environment) →
  Settings → Users + permissions → Application users → + New app user*. Microsoft's own docs note this was moved out
  of the legacy web client and that some pages still describe the old path.
- **Dataverse and Dynamics 365 Customer Engagement docs still carry pre-Entra portal wording.** The current OAuth
  page still refers to a "Keys area under API Access in the Settings for the app registration", and the server-to-
  server page still walks through `portal.azure.com`, "Required permissions" and ADAL's `AuthenticationContext`
  against `login.windows.net`. Those blades are gone. Match on meaning; use the base skill's portal path and the v2.0
  endpoints.
- **Outbound marketing was removed from Customer Insights – Journeys in May 2026.** The tables and their data were
  largely left in place and are still reachable through Dataverse, but they are no longer updated, outbound segments
  are unusable (the membership table was removed), and outbound-era events must be recreated. A connector reading
  those tables will see stale data, not an error.
- **Dynamics GP support ends 2029-12-31** (moved out from a previously announced 2029-09-30), with security updates
  and patches available until **2031-04-30**. Microsoft is steering customers to Business Central.
- **Finance & Operations user-based service protection API limits are off.** They were optional from 2023-03-31,
  disabled by default in version 10.0.35, and in **10.0.36 they are disabled on all environments and the option to
  enable them was removed**. Resource-based limits remain. The 6,000-requests-per-5-minutes figure people quote at
  F&O is the *Dataverse* number (§6).

If the Power Platform admin center or the Web API does not behave like this, stop and report what you actually see.

## 2. The per-environment resource URL — there is no API host to hardcode

This is the structural difference from every other Entra product. Microsoft Graph is always `graph.microsoft.com`.
**Dataverse is a different host for every customer, and the host is part of the OAuth scope**, so you cannot even ask
for a token until you know it.

The URL is composed as `https://{environment-name}.{region}.{base}` — for example `https://contoso.crm.dynamics.com`
for North America, `https://contoso.crm4.dynamics.com` for Europe. The region segment carries a digit that varies by
datacenter, and the documented list is:

| Region | Host segment | Region | Host segment |
| --- | --- | --- | --- |
| NAM | `crm.dynamics.com` | IND | `crm8.dynamics.com` |
| SAM | `crm2.dynamics.com` | GCC | `crm9.dynamics.com` |
| CAN | `crm3.dynamics.com` | GBR | `crm11.dynamics.com` |
| EUR | `crm4.dynamics.com` | FRA | `crm12.dynamics.com` |
| APJ | `crm5.dynamics.com` | ZAF | `crm14.dynamics.com` |
| OCE | `crm6.dynamics.com` | UAE | `crm15.dynamics.com` |
| JPN | `crm7.dynamics.com` | GER | `crm16.dynamics.com` |
| CHE | `crm17.dynamics.com` | NOR | `crm19.dynamics.com` |
| SGP | `crm20.dynamics.com` | KOR | `crm21.dynamics.com` |
| SWE | `crm22.dynamics.com` | DEU | `crm.microsoftdynamics.de` |
| GCC High | `crm.microsoftdynamics.us` | CHN | `crm.dynamics.cn` |

The list is not contiguous, it grows, and a customer can be **migrated between datacenters**, which changes their
host. Microsoft's discovery documentation says so outright: server and organization allocation changes as part of
datacenter management and load balancing, which is why a discovery service exists at all.

**So: hardcoding a region digit fails.** A connector whose fallback URL template embeds one region works for
customers in that region and returns nothing, or fails DNS, for everyone else — and the failure looks like a
credentials problem, not a routing problem. Take the host from the customer or from discovery, never from a default.

### Discovering it: the Global Discovery Service

The Global Discovery Service (GDS) is a separate OData v4 endpoint that lists the environments the **signed-in user**
can reach. Per cloud:

| Cloud | GDS host |
| --- | --- |
| Commercial | `https://globaldisco.crm.dynamics.com` |
| GCC | `https://globaldisco.crm9.dynamics.com` |
| GCC High (USG) | `https://globaldisco.crm.microsoftdynamics.us` |
| DoD | `https://globaldisco.crm.appsplatform.us` |
| China | `https://globaldisco.crm.dynamics.cn` |

```http
GET https://globaldisco.crm.dynamics.com/api/discovery/v2.0/Instances?$select=ApiUrl,FriendlyName,UrlName,State
Authorization: Bearer <token>
```

- **`ApiUrl` is the value to connect to**, and it is the one to store — note it comes back in the
  `{org}.api.crm{n}.dynamics.com` form (an `api.` label the browser URL does not have). Use `FriendlyName` for a
  picker so the user recognises the environment; the other properties are mostly for filtering.
- **It returns an array, always.** Unlike the Web API, GDS will not retrieve a single instance by key — filtering by
  `Id` or `UniqueName` still yields a one-element array. Its string filters are also **case sensitive**, which the
  Web API's are not.
- **GDS is a different endpoint from the Web API** with different behavior, and it needs its own token for its own
  resource host.
- **It is documented as user-scoped.** Its stated limitations are all about the calling user: nothing is returned if
  the account is disabled, if an environment security group filters the user out, or if access comes via delegated
  administration, and a user with no environments gets an empty list — not an error. Treat an empty list as "ask the
  customer", not as "the customer has no Dynamics".
- **For app-only there is no user to enumerate for.** Collect the environment URL from the customer's admin during
  onboarding and store it per connection. This is the normal shape of a Dynamics connector.

The customer can read their own value from the Power Platform admin center environment page, or from Power Apps →
Settings → **Developer resources**, which shows the Web API endpoint verbatim.

## 3. The application user — the headline trap

**A valid Entra token is not access to Dataverse.** For app-only (client credentials) access, the customer's admin
must create an **application user** in *each* environment and assign it at least one **security role**. Until they
do, the token exchange succeeds, the JWT looks correct, and every API call returns 401 or 403. Nothing in Entra ID
tells you this is missing.

Dataverse authorizes against a `systemuser` record, not against an app registration. The application user is that
record: an unlicensed user whose identity is your app's client ID. Mechanically, the customer's admin:

1. Opens **Power Platform admin center → Manage → Environments → (the environment) → Settings → Users + permissions
   → Application users**.
2. Selects **+ New app user**, then **+ Add an app**, and picks the Entra app registration.
3. Chooses a **Business Unit** and enters an **Email address**.
4. Assigns one or more **security roles** — and this is the step people skip, because the pane lets you save the user
   first and come back to roles. A role-less application user authenticates and can read nothing.

Five things that bite in practice:

- **It is per environment, per customer.** Production, sandbox and UAT each need their own. Your platform cannot
  self-serve any of it. Design onboarding around an out-of-band admin step and say so on the connect screen.
- **Only one application user per Entra app per environment.** You cannot create a second one with a different role
  for the same client ID.
- **Enterprise applications do not appear in the picker — only Entra *app registrations* do.** For a multitenant
  connector the customer's tenant has a service principal (an enterprise application), created when they consented,
  but not a registration. Microsoft's instruction is to **search for the multitenant application by name or client
  ID** rather than scrolling the list. This is the single most common reason an admin reports "your app isn't there".
- **Application users bypass the environment's security group.** An environment locked down by security group does
  not need your app added to that group; the role is what governs it. Do not let anyone "fix" a 403 by adding the
  app to a security group.
- **Deleting is restricted.** Only an *inactive* application user can be deleted, and records it owns must be
  reassigned first. Deactivate rather than delete when revoking.

Delegated connections do **not** need an application user — the signed-in human already is a Dataverse user with
roles. That asymmetry is worth putting in front of a customer who is choosing between the two modes.

## 4. Delegated `user_impersonation` vs app-only `.default`, and the intersection

|  | Delegated | App-only (client credentials) |
| --- | --- | --- |
| Scope requested | `https://{env}/user_impersonation` (plus `openid`, `offline_access`, …) | `https://{env}/.default` |
| Entra permission | **Access Dynamics 365 as organization users** (delegated, Dynamics CRM API) | Application permission on the same API |
| Who is the Dataverse user | The signed-in person | The **application user** (§3) |
| Refresh token | Yes, if `offline_access` was requested | No — re-request with the secret |
| Licence | The user must be licensed | The application user is unlicensed |
| Per-customer admin step | None beyond consent | Application user **and** security role, per environment |

**Effective access is the intersection of the OAuth grant and Dataverse security**, and the Dataverse half is the
tighter one. Dataverse security is role-based and row-level: a security role carries privileges per table, each with
an access level (User / Business Unit / Parent–Child Business Unit / Organization), and on top of that there is
record ownership, business-unit hierarchy, teams, and record sharing.

Consequences a connector owner should hear before they design around scopes:

- `user_impersonation` grants "whatever this user can already do" — no more. A delegated connector for a sales rep
  sees that rep's accounts and their business unit's, not the organization's. **A list that comes back short or
  empty is usually a role, not a bug.** Ask which security roles the connecting user holds before debugging code.
- There is no read-only OAuth scope. If a customer wants read-only, they give the application user a **read-only
  custom security role**. Say this explicitly in a security review; "we only request one scope" reads badly until
  you explain the role is where the least privilege lives.
- Mixing does not help. As in the base skill, any scope in the token is honored, and `/.default` must not be mixed
  with individual scopes in one request.
- The environment URL in the scope and the environment URL you call must match. A token minted for one environment is
  rejected by another, which is exactly what you want but produces a 401 that looks like an expired token.

## 5. The other family members — different APIs, not different scopes

Covered here only so you can tell them apart and route correctly. Each deserves its own run.

**Business Central.** Not Dataverse. The API is `https://api.businesscentral.dynamics.com/v2.0/{environment}/api/v2.0`
— the environment is a **path segment**, not a hostname, so unlike Dataverse the resource host is a single constant:
the scope is `https://api.businesscentral.dynamics.com/.default` for S2S. The Entra API is **Dynamics 365 Business
Central**, with application permissions `API.ReadWrite.All` (APIs and web services) and `Automation.ReadWrite.All`
(the `/api/microsoft/automation` route). Then comes BC's exact analogue of the application-user trap: the customer
opens the **Microsoft Entra applications** page *inside Business Central*, creates a record with your client ID, sets
**State = Enabled**, and assigns permission sets. Microsoft is explicit that applications **cannot** be assigned the
`SUPER` permission set; `D365 AUTOMATION` and `EXTEN. MGT. - ADMIN` cover most automation objects. BC also has its
own throttling story (§6).

**Finance & Operations.** Not Dataverse either (though an environment can be Dataverse-linked). OData lives at
`{environment-root}/data`, and the token resource is the environment root URL **with no trailing slash** — per
environment again, typically an `*.operations.dynamics.com` or `*.cloud.dynamics.com` host from Lifecycle Services.
The Entra API is **Microsoft Dynamics ERP** (`Microsoft.ERP`); Microsoft warns that searching for it by partial name
makes it look unavailable, so search the full name. Delegated permissions to select include *Access Dynamics AX
data*, *Access Dynamics AX Custom Service* and *Access Dynamics AX online as organization users*. And a third
analogue of the application user: in the app, **System administration → Setup → Microsoft Entra applications**, where
the client ID is bound to a **service account user ID** whose permissions the calls then run under. OData paging is
server-driven with a maximum page size of 10,000, and cross-company reads need `?cross-company=true`.

**Customer Insights – Data.** A separate REST API that needs an admin to **enable API access**, which mints primary
and secondary **subscription keys** on top of the OAuth token. Its Entra API is *Dynamics 365 AI for Customer
Insights* with a `user_impersonation` delegated permission. Microsoft's own recommendation is to read Customer
Insights tables through the **Dataverse** APIs instead, for better filtering, throughput and latency — which usually
means this becomes a Dataverse job after all.

**Customer Insights – Journeys** *is* Dataverse (§1) and belongs to the main path; only its table family and the
removed outbound-marketing module are special.

**Dynamics GP (and SL).** On-premises. There is no Entra OAuth path: the customer hosts a web service endpoint and
the connector authenticates to it directly with a username and password over HTTPS, scoped to a company id. It is a
credentials-collection job, not an app-registration job, and it is on a support clock (Platform state).

## 6. Versioning, limits, and what is retiring

**Versioning.** Pin `v9.2` in the Dataverse Web API path and change it deliberately. v9.0/v9.1/v9.2 behave
identically today, but Microsoft reserves version numbers for breaking changes and says not to assume a newer version
is backward compatible. Two hard request limits worth knowing: a URL may be up to **32 KB** (64 KB inside a `$batch`
body), and any **single OData segment may not exceed 260 characters** — use parameter aliases for long function
arguments rather than inlining them.

**Service protection limits (Dataverse).** Throttled calls return **429** with a **`Retry-After`** header in seconds;
honor it. The limits are evaluated **per authenticated user, per web server**, over a **five-minute (300 second)
sliding window**, on three facets:

| Measure | Default limit per web server |
| --- | --- |
| Number of requests | **6,000** within the 5-minute window |
| Combined execution time | **20 minutes (1,200 seconds)** within the 5-minute window |
| Concurrent requests | **52 or higher** |

Four things that follow:

- **Per user** is the important word. An app-only connector funnels *all* its traffic through one application user,
  so it concentrates everything against a single user's budget — the same shape Microsoft calls out for portals. A
  delegated connector spreads naturally across users.
- **Most environments have several web servers** (a trial has one), and the number scales with licences, so observed
  headroom varies between customers and is not something to calibrate against.
- **Batching does not buy you a free ride.** A `$batch` may hold up to 1,000 operations and dodges the request count,
  but the execution-time and concurrency facets exist precisely to catch that. Exceeding concurrency errors
  *immediately*; the other two take effect after the window.
- Sustained demanding traffic **extends** the Retry-After duration. Back off to a steady rate rather than retrying
  hard; plug-ins and custom workflow activities triggered by your calls do not count as requests, but their
  computation time is added to the request that triggered them.

**Business Central limits** are a different model: **429** on throttling with no documented `Retry-After` — the
guidance is client-side backoff — plus a hard **10-minute request execution limit** that returns **504 Gateway
Timeout**. Split long requests; prefer webhooks over polling; use `$filter`, `$expand` and deep inserts to cut call
counts.

**Finance & Operations limits**: user-based limits are disabled from version 10.0.36 (Platform state); resource-based
limits still throttle on environment resource utilization. Do not quote the Dataverse 6,000/5-minutes figure at an
F&O environment — it is not the same service, and the page people cite for it now says the user-based limits are off.

**Retiring / retired**: outbound marketing in Customer Insights – Journeys (removed May 2026); Dynamics GP support
(2029-12-31, security patches to 2031-04-30). ADAL is long dead — the Dataverse pages that still show
`AuthenticationContext` and `login.windows.net` are stale, not a supported alternative.

## 7. Product fact — the connectors as of 2026-09-20

There are **several separate Dynamics connectors in this family**, not one. Dataverse-backed surfaces each have their
own connector: **Sales**, **Customer Engagement**, **Contact Center** and **Customer Insights – Journeys**. Outside
Dataverse there are separate connectors for **Business Central**, **Finance & Operations** and **Dynamics GP**. The
pattern across them:

- **All the OAuth ones are delegated, not app-only.** Every one declares the same authorization-code shape: a
  `prompt=select_account` authorize with a space-delimited scope list, a form-POST token exchange with client ID and
  secret, and a refresh call on the same parameters. None of them ships a client-credentials path, so **none of them
  needs the application-user step** — which also means none of them can run unattended. If a customer asks for
  app-only sync, that is new work, and §3 is the cost.
- **The per-object scope sets are uniform and minimal**: `openid`, `email`, `offline_access` and a bare
  `user_impersonation`, for every object, read and write alike. There is a separate sign-in-only scope set of
  `openid`, `email`, `profile` and `User.Read`. Least privilege genuinely lives in the customer's security role (§4),
  and a security reviewer should be told that rather than left to infer it from a one-scope request.
- **The bare `user_impersonation` is rewritten to a resource-scoped scope at authorize time** — prefixed with the
  customer's environment host for the Dataverse connectors, and replaced outright with the fixed Business Central
  resource plus `/.default` for that one. This is the §2 rule implemented, and it is why the environment URL must be
  collected before the authorize redirect, not after.
- **The environment URL is collected from the customer as a connect-time input**, labelled as the environment URL
  (Finance & Operations asks for the bare host, Business Central asks instead for an environment *name*, matching the
  path-segment shape in §5). There is no Global Discovery Service call anywhere; discovery is manual. Accepted input
  is normalised, and at least one connector's fallback URL templates **hardcode a single region digit** — fine for
  that region, wrong for the rest (§2). Worth confirming before you assume a customer outside that region works.
- **Authorization is sent to `/organizations`** at run time even where the stored metadata says `/common`, which is
  correct for a work/school-only product — `/common` would let a personal Microsoft account start a flow it can
  never finish.
- **Dynamics GP is not OAuth at all**: it collects a web service URL, a company id, a username and a password, and
  authenticates with HTTP Basic against a customer-hosted endpoint (§5).
- **Two dated documentation defects** in the connector metadata: one Dataverse-backed surface cites the *Finance &
  Operations* service-protection page for the Dataverse 6,000/5-minute figure (wrong page, and that page now says F&O
  user-based limits are off — §6), and one product's linked API documentation URL currently 404s.

**Confirm all of this with the connector's owner before you register anything.** Scope sets, hosts and auth modes
change; this is a snapshot, not a contract.

## 8. Verify the Dynamics-specific parts

Run the base skill's end-to-end check first (authorize from a **different tenant**, confirm a `refresh_token` came
back, decode the token's `scp`/`roles`). Then:

1. Confirm the token's audience is the **customer's environment URL**, not Graph and not your own environment. This
   is the fastest way to catch a scope that was built from a default host.
2. Call `GET {env}/api/data/v9.2/WhoAmI`. It is the cheapest possible proof and it returns the `UserId` the call is
   running as — for app-only that is the application user, which is how you confirm §3 actually happened.
3. Then read one business table you actually need. `WhoAmI` succeeding proves authentication; it does **not** prove
   the security role grants anything.
4. For app-only, confirm access is **denied before** the security role is assigned and **allowed after**. If it
   works before, someone assigned a broader role than agreed.
5. If the customer has more than one environment, test the second one. Nothing generalizes across environments.
6. Force a token refresh and repeat step 3 — Dataverse access tokens last about an hour.

| Symptom | Cause |
| --- | --- |
| Token exchange succeeds, every call is 401/403 | App-only with no application user, or one with no security role (§3) |
| The admin says your app "isn't in the list" when creating the app user | The picker shows app *registrations*; search the multitenant app by name or client ID (§3) |
| Works in the customer's production, 401 in their sandbox | Application user and role are per environment (§3) |
| `WhoAmI` works, business tables return 403 | The security role lacks that table's privilege, or the access level is too narrow (§4) |
| Delegated calls return far fewer rows than the admin expects | Row-level security: ownership, business unit or access level, not a query bug (§4) |
| DNS failure or 404 for one customer, fine for others | A hardcoded region digit in the host (§2) |
| A customer that used to work stops resolving | Environment migrated between datacenters; rediscover the URL (§2) |
| Discovery returns an empty array | Disabled account, environment security group filtering, or delegated-admin access — not "no Dynamics" (§2) |
| `invalid_scope` / `AADSTS70011` on authorize | Bare `user_impersonation` sent unqualified, or `/.default` mixed with individual scopes (§4) |
| Auth succeeds but no `refresh_token` | `offline_access` not requested (base skill) |
| 429 under load, then longer and longer pauses | Service protection; honor `Retry-After` and back off to a steady rate (§6) |
| 504 on a Business Central call | The 10-minute request execution limit; split the request (§6) |
| Customer Insights – Journeys data looks frozen | Outbound-marketing tables, removed May 2026 and no longer updated (Platform state) |

## Hand off — the Dynamics additions

The base skill's hand-off list applies. Add to the closing summary: **which Dynamics product** and therefore which
API surface; whether the connection is **delegated or app-only**; for each customer environment, its **exact
environment URL** and how it was obtained (admin-supplied or discovered); for app-only, **who created the application
user, in which environment, with which security role, and when**; the **Web API version** pinned; and the cloud, if
it is not commercial.

## Stop and ask

On top of the base skill's conditions, hand back to a human when:

- The product has not been identified. Registering an Entra app for "Dynamics 365" without knowing whether it is
  Dataverse, Business Central, Finance & Operations or GP produces a credential that fits nothing.
- App-only is proposed and nobody has confirmed a customer admin will create the application user and assign a role,
  in every environment, at every customer. That is an onboarding commitment, not a config detail.
- Someone proposes defaulting or guessing the environment URL, or hardcoding a region digit.
- A customer asks for read-only and the plan is to express it in OAuth. It has to be a security role, and designing
  that role is the customer's decision (§4).
- The security role being proposed is broad (System Administrator, or any role chosen because it "just works").
  Over-privileging an application user is the finding a customer security review will lead with.
- An environment security group is being changed to fix a 403 — it is the wrong lever (§3).
- A customer is on a national cloud (GCC, GCC High, DoD, China): different Dataverse *and* discovery hosts, different
  authority, and publisher verification is unavailable.
- Dynamics GP or SL is in scope, or an on-premises Dynamics 365 Customer Engagement deployment — neither is an Entra
  OAuth job, and GP is on an end-of-support clock.
- A plan depends on outbound marketing in Customer Insights – Journeys. It was removed; the plan needs rewriting.

## References

The base skill carries the shared Entra references. These are the Dynamics/Dataverse-specific ones; verified
2026-09-20, every link returned HTTP 200.

- Use the Microsoft Dataverse Web API — https://learn.microsoft.com/en-us/power-apps/developer/data-platform/webapi/overview
- Use OAuth authentication with Microsoft Dataverse (`user_impersonation` vs `.default`) — https://learn.microsoft.com/en-us/power-apps/developer/data-platform/authenticate-oauth
- Authentication with Dataverse (overview) — https://learn.microsoft.com/en-us/power-apps/developer/data-platform/authentication
- Walkthrough: register an app with Microsoft Entra ID (Dataverse) — https://learn.microsoft.com/en-us/power-apps/developer/data-platform/walkthrough-register-app-azure-active-directory
- Discover user organizations (Global Discovery Service) — https://learn.microsoft.com/en-us/power-apps/developer/data-platform/discovery-service
- Compose HTTP requests and handle errors (Web API URL parts, URL/segment limits) — https://learn.microsoft.com/en-us/power-apps/developer/data-platform/webapi/compose-http-requests-handle-errors
- View developer resources (find the environment's Web API endpoint) — https://learn.microsoft.com/en-us/power-apps/developer/data-platform/view-download-developer-resources
- Dataverse Web API versions — https://learn.microsoft.com/en-us/power-apps/developer/data-platform/webapi/web-api-versions
- Datacenter regions (the `crm{n}` host table) — https://learn.microsoft.com/en-us/power-platform/admin/new-datacenter-regions
- Environments overview — https://learn.microsoft.com/en-us/power-platform/admin/environments-overview
- Manage application users in the Power Platform admin center — https://learn.microsoft.com/en-us/power-platform/admin/manage-application-users
- Use multi-tenant server-to-server authentication — https://learn.microsoft.com/en-us/power-apps/developer/data-platform/use-multi-tenant-server-server-authentication
- Use single-tenant server-to-server authentication — https://learn.microsoft.com/en-us/power-apps/developer/data-platform/use-single-tenant-server-server-authentication
- Security roles and privileges — https://learn.microsoft.com/en-us/power-platform/admin/security-roles-privileges
- Dataverse security concepts — https://learn.microsoft.com/en-us/power-platform/admin/wp-security-cds
- Dataverse database security — https://learn.microsoft.com/en-us/power-platform/admin/database-security
- Dataverse service protection API limits — https://learn.microsoft.com/en-us/power-apps/developer/data-platform/api-limits
- Dynamics 365 table/entity reference — https://learn.microsoft.com/en-us/dynamics365/developer/reference/about-entity-reference
- Business Central: API endpoints — https://learn.microsoft.com/en-us/dynamics365/business-central/dev-itpro/api-reference/v2.0/endpoints-apis-for-dynamics
- Business Central: using OAuth to authorize web services — https://learn.microsoft.com/en-us/dynamics365/business-central/dev-itpro/webservices/authenticate-web-services-using-oauth
- Business Central: service-to-service authentication — https://learn.microsoft.com/en-us/dynamics365/business-central/dev-itpro/administration/automation-apis-using-s2s-authentication
- Business Central: working with API limits — https://learn.microsoft.com/en-us/dynamics365/business-central/dev-itpro/api-reference/v2.0/dynamics-rate-limits
- Finance & Operations: service endpoints overview (Entra app + service account) — https://learn.microsoft.com/en-us/dynamics365/fin-ops-core/dev-itpro/data-entities/services-home-page
- Finance & Operations: OData — https://learn.microsoft.com/en-us/dynamics365/fin-ops-core/dev-itpro/data-entities/odata
- Finance & Operations: service protection API limits — https://learn.microsoft.com/en-us/dynamics365/fin-ops-core/dev-itpro/data-entities/service-protection-api-limits
- Finance & Operations: priority-based throttling — https://learn.microsoft.com/en-us/dynamics365/fin-ops-core/dev-itpro/data-entities/priority-based-throttling
- Customer Insights – Data APIs — https://learn.microsoft.com/en-us/dynamics365/customer-insights/data/apis
- Customer Insights – Journeys: outbound marketing removal — https://learn.microsoft.com/en-us/dynamics365/customer-insights/journeys/developer/marketing-developer-guide
- Dynamics 365 Contact Center documentation — https://learn.microsoft.com/en-us/dynamics365/contact-center/
- Dynamics 365 Customer Service implementation overview — https://learn.microsoft.com/en-us/dynamics365/customer-service/implement/overview
- Dynamics GP documentation (end-of-support dates) — https://learn.microsoft.com/en-us/dynamics-gp/
