Summary and recommendation
The Dynamics 365 Dataverse Web API (v9.2) exposes the `systemusers` entity collection for user lifecycle management, authenticated via OAuth 2.0 against Microsoft Entra ID.
User provisioning is a two-phase operation: a Dynamics 365 license must first be assigned via Microsoft Graph (`POST /v1.0/users/{userId}/assignLicense`), after which the `systemuser` record is auto-created or can be explicitly POSTed to `/api/data/v9.2/systemusers`.
The API alone cannot grant a license - attempting to activate a `systemuser` record without a valid license assignment results in `isdisabled=true` and no login access. All requests require `OData-MaxVersion: 4.0` and `OData-Version: 4.0` headers; omitting them produces unpredictable errors.
The base URL is region-specific (`.crm.dynamics.com` for North America, `.crm4.dynamics.com` for EMEA, `.crm5.dynamics.com` for APAC) and should always be resolved from the environment's discovery endpoint rather than hardcoded.
API quick reference
| Has user API | Yes |
| Auth method | OAuth 2.0 (Azure AD / Microsoft Entra ID) |
| Base URL | Official docs |
| SCIM available | Yes |
| SCIM plan required | Requires Microsoft Entra ID (Azure AD) P1 or P2 for automated provisioning; Dynamics 365 license required for target users. Entra ID P1 is included in Microsoft 365 E3/E5 and many Dynamics 365 bundles. |
Authentication
Auth method: OAuth 2.0 (Azure AD / Microsoft Entra ID)
Setup steps
- Register an application in Microsoft Entra ID (Azure AD) at https://portal.azure.com/#view/Microsoft_AAD_RegisteredApps
- Grant the app API permissions: Dynamics CRM > user_impersonation (delegated) or application-level permissions
- Generate a client secret or configure certificate credentials for the app registration
- Obtain an access token via the OAuth 2.0 client credentials or authorization code flow from https://login.microsoftonline.com/{tenant_id}/oauth2/v2.0/token
- Pass the Bearer token in the Authorization header on all API requests
- Ensure the calling user or service principal has a Dynamics 365 security role assigned (e.g., System Administrator)
Required scopes
| Scope | Description | Required for |
|---|---|---|
| https://org.crm.dynamics.com/user_impersonation | Delegated scope allowing the app to access Dynamics 365 on behalf of the signed-in user | All Dataverse Web API calls in delegated (user) context |
| https://graph.microsoft.com/User.Read.All | Microsoft Graph scope to read Entra ID user profiles; used when syncing AAD users into Dynamics 365 | Reading Azure AD user data prior to provisioning into Dynamics 365 |
| https://graph.microsoft.com/Directory.ReadWrite.All | Microsoft Graph scope for managing directory objects; required for license assignment via Graph | Assigning Dynamics 365 licenses to users via Microsoft Graph |
User object / data model
| Field | Type | Description | On create | On update | Notes |
|---|---|---|---|---|---|
| systemuserid | Guid | Unique identifier for the system user (primary key) | auto-generated | immutable | Used as the record ID in all API URLs |
| domainname | String | User's UPN / login name (e.g., user@contoso.com) | required | optional | Must match the Azure AD UPN for SSO to function |
| firstname | String | User's first name | required | optional | |
| lastname | String | User's last name | required | optional | |
| fullname | String | Computed full name (read-only) | read-only | read-only | Derived from firstname + lastname |
| internalemailaddress | String | Primary email address of the user | required | optional | |
| title | String | Job title of the user | optional | optional | |
| mobilephone | String | Mobile phone number | optional | optional | |
| businessunitid | Lookup (businessunit) | Business unit the user belongs to | required | optional | Defaults to root business unit if not specified |
| isdisabled | Boolean | Whether the user account is disabled in Dynamics 365 | optional (default false) | optional | Set to true to deactivate; does not delete the record |
| accessmode | OptionSet (Integer) | License/access type: 0=Read-Write, 1=Administrative, 2=Read, 3=Support User, 4=Non-interactive, 5=Delegated Admin | optional | optional | Non-interactive users (4) do not consume a full license |
| caltype | OptionSet (Integer) | Client Access License type: 0=Professional, 1=Administrative, 2=Basic, 3=Device Professional, 4=Device Basic, 11=Team Member | optional | optional | Determines which Dynamics 365 features the user can access |
| azureactivedirectoryobjectid | Guid | Azure AD Object ID of the user; links Dynamics record to Entra ID identity | auto-populated on sync | read-only after set | Required for SSO and license assignment |
| territoryid | Lookup (territory) | Sales territory assigned to the user | optional | optional | |
| defaultmailbox | Lookup (mailbox) | Default mailbox record associated with the user | auto-created | optional | |
| userlicensetype | Integer | Numeric license type indicator | optional | optional | Deprecated in favor of caltype in newer API versions |
| positionid | Lookup (position) | Organizational position/hierarchy node for the user | optional | optional | |
| parentsystemuserid | Lookup (systemuser) | Manager of this user (for org hierarchy) | optional | optional | |
| systemuserroles_association | Many-to-Many (role) | Security roles assigned to the user | via $ref association after create | via $ref association/disassociation | Roles are assigned via a separate POST to the association endpoint, not inline on the user record |
Core endpoints
List users
- Method: GET
- URL:
https://{org}.api.crm.dynamics.com/api/data/v9.2/systemusers?$select=systemuserid,domainname,fullname,isdisabled&$top=50 - Watch out for: Deleted/disabled users are still returned unless filtered with $filter=isdisabled eq false. Use $select to avoid fetching all 200+ fields.
Request example
GET /api/data/v9.2/systemusers?$select=systemuserid,domainname,fullname,isdisabled&$top=50
Authorization: Bearer {token}
OData-MaxVersion: 4.0
OData-Version: 4.0
Response example
{
"value": [
{
"systemuserid": "a1b2c3d4-...",
"domainname": "user@contoso.com",
"fullname": "Jane Doe",
"isdisabled": false
}
],
"@odata.nextLink": "https://{org}.api.crm.dynamics.com/api/data/v9.2/systemusers?$skiptoken=..."
}
Get single user
- Method: GET
- URL:
https://{org}.api.crm.dynamics.com/api/data/v9.2/systemusers({systemuserid}) - Watch out for: You cannot look up a user by domainname directly in the URL; use $filter=domainname eq 'user@contoso.com' on the collection endpoint instead.
Request example
GET /api/data/v9.2/systemusers(a1b2c3d4-0000-0000-0000-000000000001)
Authorization: Bearer {token}
Response example
{
"systemuserid": "a1b2c3d4-...",
"domainname": "user@contoso.com",
"firstname": "Jane",
"lastname": "Doe",
"internalemailaddress": "user@contoso.com",
"isdisabled": false,
"accessmode": 0
}
Create user
- Method: POST
- URL:
https://{org}.api.crm.dynamics.com/api/data/v9.2/systemusers - Watch out for: The user must already exist in Azure AD / Entra ID with a valid Dynamics 365 license assigned before the record can be activated. Creating the Dataverse record alone does not provision a license.
Request example
POST /api/data/v9.2/systemusers
Authorization: Bearer {token}
Content-Type: application/json
{
"domainname": "newuser@contoso.com",
"firstname": "John",
"lastname": "Smith",
"internalemailaddress": "newuser@contoso.com",
"businessunitid@odata.bind": "/businessunits({buId})"
}
Response example
HTTP 204 No Content
OData-EntityId: https://{org}.api.crm.dynamics.com/api/data/v9.2/systemusers(new-guid)
Update user
- Method: PATCH
- URL:
https://{org}.api.crm.dynamics.com/api/data/v9.2/systemusers({systemuserid}) - Watch out for: PATCH is a partial update (only supplied fields are changed). Do not use PUT; it is not supported for systemuser.
Request example
PATCH /api/data/v9.2/systemusers(a1b2c3d4-...)
Authorization: Bearer {token}
Content-Type: application/json
{
"title": "Senior Manager",
"mobilephone": "+1-555-0100"
}
Response example
HTTP 204 No Content
Disable (deactivate) user
- Method: PATCH
- URL:
https://{org}.api.crm.dynamics.com/api/data/v9.2/systemusers({systemuserid}) - Watch out for: Dynamics 365 does not support hard-deleting system users via the API. Deactivation (isdisabled=true) is the only supported removal path. The user record is retained for audit/ownership history.
Request example
PATCH /api/data/v9.2/systemusers(a1b2c3d4-...)
Authorization: Bearer {token}
Content-Type: application/json
{"isdisabled": true}
Response example
HTTP 204 No Content
Assign security role to user
- Method: POST
- URL:
https://{org}.api.crm.dynamics.com/api/data/v9.2/systemusers({systemuserid})/systemuserroles_association/$ref - Watch out for: Role IDs are environment-specific GUIDs. The same role name will have a different GUID in each Dynamics 365 environment (dev/test/prod). Always query /roles to resolve IDs dynamically.
Request example
POST /api/data/v9.2/systemusers(a1b2c3d4-...)/systemuserroles_association/$ref
Authorization: Bearer {token}
Content-Type: application/json
{
"@odata.id": "https://{org}.api.crm.dynamics.com/api/data/v9.2/roles({roleId})"
}
Response example
HTTP 204 No Content
Remove security role from user
- Method: DELETE
- URL:
https://{org}.api.crm.dynamics.com/api/data/v9.2/systemusers({systemuserid})/systemuserroles_association({roleId})/$ref - Watch out for: Removing all roles from a user does not disable them; the user retains access until isdisabled is set to true or the Entra ID license is revoked.
Request example
DELETE /api/data/v9.2/systemusers(a1b2c3d4-...)/systemuserroles_association(role-guid)/$ref
Authorization: Bearer {token}
Response example
HTTP 204 No Content
List security roles for user
- Method: GET
- URL:
https://{org}.api.crm.dynamics.com/api/data/v9.2/systemusers({systemuserid})/systemuserroles_association?$select=roleid,name - Watch out for: This returns roles directly assigned to the user. Roles inherited via team membership are not included in this response.
Request example
GET /api/data/v9.2/systemusers(a1b2c3d4-...)/systemuserroles_association?$select=roleid,name
Authorization: Bearer {token}
Response example
{
"value": [
{
"roleid": "role-guid-1",
"name": "Salesperson"
},
{
"roleid": "role-guid-2",
"name": "System Customizer"
}
]
}
Rate limits, pagination, and events
- Rate limits: Dynamics 365 / Dataverse enforces service protection API limits per user per 5-minute sliding window. Limits apply per environment and are not plan-tiered in the traditional sense.
- Rate-limit headers: Yes
- Retry-After header: Yes
- Rate-limit notes: When limits are exceeded, HTTP 429 is returned with a Retry-After header. Headers x-ms-ratelimit-burst-remaining-xrm-requests and x-ms-ratelimit-time-remaining-xrm-requests indicate remaining quota. Limits are per authenticated user, not per app registration. Service principal (S2S) calls have separate higher limits.
- Pagination method: token
- Default page size: 5000
- Max page size: 5000
- Pagination pointer: $top (max 5000); continuation via @odata.nextLink token in response
| Plan | Limit | Concurrent |
|---|---|---|
| All Dynamics 365 / Dataverse environments | 6,000 API requests per user per 5-minute window; 52,000 requests per user per 24-hour period; max execution time 20 minutes per 5-minute window | 52 |
- Webhooks available: Yes
- Webhook notes: Dynamics 365 / Dataverse supports event-driven notifications via Webhooks (registered as Service Endpoints) and Azure Service Bus/Event Hub integration. Webhooks can be triggered on Create, Update, Delete, and custom message events on any entity including systemuser.
- Alternative event strategy: Azure Service Bus or Azure Event Hub can be used as an alternative to HTTP webhooks for more reliable async processing. Plugin steps registered in the Plug-in Registration Tool can also trigger custom logic on user events.
- Webhook events: Create (systemuser), Update (systemuser), Delete (systemuser), Associate (systemuserroles_association), Disassociate (systemuserroles_association), SetState (systemuser enable/disable)
SCIM API status
SCIM available: Yes
SCIM version: 2.0
Plan required: Requires Microsoft Entra ID (Azure AD) P1 or P2 for automated provisioning; Dynamics 365 license required for target users. Entra ID P1 is included in Microsoft 365 E3/E5 and many Dynamics 365 bundles.
Endpoint: SCIM provisioning is handled by Microsoft Entra ID's provisioning service targeting Dynamics 365 as a connected app. The SCIM endpoint is managed by Entra, not exposed as a standalone Dynamics 365 URL. Entra provisioning job URL pattern: https://graph.microsoft.com/v1.0/servicePrincipals/{spId}/synchronization/jobs
Supported operations: Create user (provision Entra ID user into Dynamics 365), Update user attributes (sync profile changes), Deactivate user (set isdisabled=true on systemuser), Group-based provisioning (assign/remove Dynamics 365 app roles via Entra groups)
Limitations:
- SCIM provisioning for Dynamics 365 is implemented via the Entra ID Application Provisioning service (gallery app), not a direct SCIM endpoint on the Dynamics 365 tenant
- Provisioning does not assign Dynamics 365 security roles; role assignment must be done separately via the Dataverse Web API or manually
- Dynamics 365 Business Central uses a separate provisioning mechanism and is not covered by the same Entra gallery app
- Hard delete of users is not supported; deprovisioning sets isdisabled=true only
- Attribute mapping is configurable but limited to fields exposed by the Dataverse connector in Entra provisioning
- SSO (Entra ID / SAML or OIDC) must be configured as a prerequisite for SCIM provisioning to function correctly
Common scenarios
Three scenarios cover the majority of programmatic identity lifecycle work against Dynamics 365.
First, provisioning: assign the license via Graph, wait for Entra sync, then POST the systemuser record if not auto-created, resolve the systemuserid by filtering on domainname, and POST each security role to /systemusers({id})/systemuserroles_association/$ref - role GUIDs are environment-specific and must be resolved dynamically via `GET /roles?
$filter=name eq 'RoleName'for every target environment. Second, deprovisioning: PATCHisdisabled=trueon thesystemuserrecord, invoke theReassignObjectsSystemUseraction for owned records, then separately revoke the license via Graph - hard DELETE on/systemusers` returns an error.
deactivation is the only supported offboarding path. Third, SCIM automation via Entra ID: configure the Dynamics 365 gallery app in Entra Enterprise Applications with Provisioning Mode set to Automatic.
note that Entra SCIM handles create and deactivate but does not assign Dynamics 365 security roles - a separate Power Automate flow or Azure Function triggered on user creation is required to complete role assignment.
This three-scenario pattern maps directly to the identity graph maintained by an orchestration layer: Entra ID as the authoritative identity source, Graph for license state, and the Dataverse Web API for environment-level access and role membership.
Provision a new Dynamics 365 user from Entra ID
- Assign a Dynamics 365 license to the user in Microsoft 365 Admin Center or via Microsoft Graph: POST https://graph.microsoft.com/v1.0/users/{userId}/assignLicense with the Dynamics 365 SKU ID.
- Wait for Entra ID to sync the license (typically within minutes); the systemuser record is auto-created in Dynamics 365 with isdisabled=false.
- If the record is not auto-created, POST to /api/data/v9.2/systemusers with domainname, firstname, lastname, internalemailaddress, and businessunitid@odata.bind.
- Retrieve the new systemuserid: GET /api/data/v9.2/systemusers?$filter=domainname eq 'newuser@contoso.com'&$select=systemuserid.
- Assign required security roles: POST /api/data/v9.2/systemusers({systemuserid})/systemuserroles_association/$ref with the role @odata.id for each role.
- Optionally set businessunitid, title, and parentsystemuserid via PATCH /api/data/v9.2/systemusers({systemuserid}).
Watch out for: License assignment must precede or accompany Dataverse record creation. A systemuser record without a valid license will have isdisabled=true and cannot log in.
Deprovision (offboard) a Dynamics 365 user
- Resolve the systemuserid: GET /api/data/v9.2/systemusers?$filter=domainname eq 'leavinguser@contoso.com'&$select=systemuserid,isdisabled.
- Disable the user in Dynamics 365: PATCH /api/data/v9.2/systemusers({systemuserid}) with body {"isdisabled": true}.
- Reassign owned records if required using the ReassignObjectsSystemUser action or manually via bulk update.
- Revoke the Dynamics 365 license in Microsoft 365 Admin Center or via Microsoft Graph DELETE /users/{userId}/assignLicense.
- Optionally disable the Entra ID account: PATCH https://graph.microsoft.com/v1.0/users/{userId} with {"accountEnabled": false}.
Watch out for: Disabling in Dynamics 365 (step 2) and revoking the license (step 4) are independent operations. Doing only one may leave the user partially accessible. Records owned by the disabled user remain assigned to them until explicitly reassigned.
Automate SCIM provisioning via Microsoft Entra ID
- In the Azure Portal, navigate to Entra ID > Enterprise Applications and add the 'Dynamics 365' gallery application.
- Configure Single Sign-On (SAML or OIDC) for the application as a prerequisite.
- Navigate to Provisioning > set Provisioning Mode to 'Automatic'.
- Enter the Dynamics 365 tenant URL and authenticate with a System Administrator account to authorize Entra provisioning.
- Configure attribute mappings (Entra ID user attributes → Dataverse systemuser fields) under Mappings.
- Define scope: assign users/groups to the Enterprise Application to control who gets provisioned.
- Enable provisioning and monitor the provisioning logs in Entra ID for errors.
- Security role assignment post-provisioning must be handled separately via the Dataverse Web API or Power Automate flow triggered on user creation.
Watch out for: Entra SCIM provisioning creates and deactivates systemuser records but does not assign Dynamics 365 security roles. A separate automation (Power Automate, Azure Function, or manual step) is required to assign roles after provisioning.
Why building this yourself is a trap
The most common API integration failure is treating Dynamics 365 as a single-surface system. In practice, the identity graph spans three layers - Entra ID (identity and license), Dataverse Web API (environment access and RBAC), and Business Central's own provisioning surface - and writes to one layer do not propagate to the others.
Service Protection Limits enforce 6,000 requests per user per 5-minute window with HTTP 429 and a Retry-After header; S2S (service principal) calls have higher limits but are still per-environment capped, so bulk provisioning jobs must implement exponential backoff.
Role inheritance via team membership is invisible to GET /systemusers({id})/systemuserroles_association, which returns only directly assigned roles - integrations that audit effective permissions without querying team memberships will produce incomplete results. Filtering on the computed fullname field is unreliable; filter on firstname and lastname separately.
Finally, pagination uses @odata.nextLink tokens with a hard cap of 5,000 records per page; integrations that assume a single response contains all users will silently miss records in large tenants.
Automate Microsoft Dynamics 365 workflows without one-off scripts
Stitchflow builds and maintains end-to-end IT automation across your SaaS stack, including apps without APIs. Built for exactly how your company works, with human approvals where they matter.