Summary and recommendation
Microsoft Graph API (base URL: https://graph.microsoft.com/v1.0) is the authoritative surface for programmatic Microsoft 365 user management. Auth is OAuth 2.0 via the Microsoft identity platform; app-only flows use client credentials against a registered Entra ID application with admin-consented application permissions.
Core scopes for full lifecycle management are User.ReadWrite.All and Directory.ReadWrite.All - both require explicit tenant-wide admin consent before any call succeeds.
The Graph user object is the central node in the Microsoft 365 identity graph, linking licenses (assignedLicenses), directory roles, group memberships, authentication methods, and manager relationships.
This makes Graph the right integration point for building an identity graph across the Microsoft 365 tenant - correlating user state, license entitlements, and group-scoped access in a single traversal.
Pagination uses @odata.nextLink continuation tokens with a default page size of 100 and a maximum of 999; numeric offset pagination is not supported and skip tokens must not be constructed manually.
API quick reference
| Has user API | Yes |
| Auth method | OAuth 2.0 (Microsoft identity platform / Entra ID) |
| Base URL | Official docs |
| SCIM available | Yes |
| SCIM plan required | Microsoft Entra ID P1 or P2 (included in Microsoft 365 Business Premium, E3, E5; add-on for Business Basic/Standard) |
Authentication
Auth method: OAuth 2.0 (Microsoft identity platform / Entra ID)
Setup steps
- Register an application in Microsoft Entra admin center (https://entra.microsoft.com) under App registrations.
- Configure a client secret or certificate under Certificates & secrets.
- Grant required Microsoft Graph API permissions (delegated or application) under API permissions.
- Admin must grant tenant-wide admin consent for application permissions.
- Obtain an access token via OAuth 2.0 client credentials flow (app-only) or authorization code flow (delegated): POST https://login.microsoftonline.com/{tenant_id}/oauth2/v2.0/token
- Include the token as a Bearer token in the Authorization header of all Graph API requests.
Required scopes
| Scope | Description | Required for |
|---|---|---|
| User.Read.All | Read all users' full profiles. | List users, get user details (application or delegated) |
| User.ReadWrite.All | Read and write all users' full profiles. | Create, update, delete users (application or delegated) |
| Directory.Read.All | Read directory data including users, groups, and organizational contacts. | Read directory objects broadly |
| Directory.ReadWrite.All | Read and write directory data. | Write directory objects broadly; required for some user management operations |
| User.Invite.All | Invite guest users to the organization. | Sending B2B guest invitations |
| UserAuthenticationMethod.ReadWrite.All | Read and write all users' authentication methods. | Managing MFA methods, password reset |
User object / data model
| Field | Type | Description | On create | On update | Notes |
|---|---|---|---|---|---|
| id | String (GUID) | Unique identifier for the user object in Entra ID. | auto-generated | immutable | Use this as the stable identifier; UPN can change. |
| userPrincipalName | String | UPN (login name), typically user@domain.com. | required | updatable | Must be a verified domain in the tenant. |
| displayName | String | Display name shown in address books and UI. | required | updatable | |
| givenName | String | First name. | optional | updatable | |
| surname | String | Last name. | optional | updatable | |
| String | Primary SMTP email address. | auto-assigned for licensed users | limited; use Exchange cmdlets for full control | Read-only via Graph for cloud-only users in many cases; managed by Exchange. | |
| mailNickname | String | Mail alias (portion before @). | required | updatable | |
| accountEnabled | Boolean | Whether the account is enabled for sign-in. | required (set true/false) | updatable | Set to false to disable/block sign-in without deleting the user. |
| passwordProfile | Object | Contains password and forceChangePasswordNextSignIn flag. | required | updatable (to reset password) | passwordProfile.password must meet tenant complexity policy. |
| usageLocation | String (ISO 3166-1 alpha-2) | Country code required before assigning licenses. | optional but required before license assignment | updatable | e.g., 'US', 'GB'. Must be set before calling assignLicense. |
| assignedLicenses | Array of assignedLicense | Licenses assigned to the user. | not set at creation; use assignLicense action | via POST /users/{id}/assignLicense | Read via GET; write via dedicated assignLicense endpoint. |
| jobTitle | String | User's job title. | optional | updatable | |
| department | String | Department name. | optional | updatable | |
| mobilePhone | String | Primary mobile phone number. | optional | updatable | |
| officeLocation | String | Office location. | optional | updatable | |
| manager | directoryObject (relationship) | User's manager. | not set at creation | via PUT /users/{id}/manager/$ref | Separate endpoint; not a direct property in PATCH body. |
| proxyAddresses | Array of String | All email addresses (SMTP aliases) for the user. | auto-populated | limited via Graph; use Exchange Online PowerShell for full alias management | Read-only in most Graph contexts for cloud mailboxes. |
| userType | String | 'Member' or 'Guest'. | defaults to 'Member' | updatable with appropriate permissions | |
| createdDateTime | DateTimeOffset | Timestamp when the user object was created. | auto-generated | immutable | |
| onPremisesSyncEnabled | Boolean | True if the user is synced from on-premises AD via Entra Connect. | auto-set | immutable via Graph | Synced users cannot have most attributes updated via Graph; changes must be made on-premises. |
Core endpoints
List users
- Method: GET
- URL:
https://graph.microsoft.com/v1.0/users - Watch out for: Default response omits many fields; use $select to specify needed fields. Without $select, only a subset of properties is returned.
Request example
GET /v1.0/users?$top=100&$select=id,displayName,userPrincipalName,accountEnabled
Authorization: Bearer {token}
Response example
{
"value": [
{"id":"abc-123","displayName":"Jane Doe","userPrincipalName":"jane@contoso.com","accountEnabled":true}
],
"@odata.nextLink":"https://graph.microsoft.com/v1.0/users?$skiptoken=..."
}
Get user
- Method: GET
- URL:
https://graph.microsoft.com/v1.0/users/{id | userPrincipalName} - Watch out for: Using UPN as identifier can fail if UPN contains special characters; prefer object ID (GUID) for reliability.
Request example
GET /v1.0/users/abc-123
Authorization: Bearer {token}
Response example
{
"id": "abc-123",
"displayName": "Jane Doe",
"userPrincipalName": "jane@contoso.com",
"accountEnabled": true,
"jobTitle": "Engineer"
}
Create user
- Method: POST
- URL:
https://graph.microsoft.com/v1.0/users - Watch out for: Creating a user does not assign a license. Set usageLocation before calling assignLicense or the license assignment will fail.
Request example
POST /v1.0/users
Content-Type: application/json
{
"displayName":"John Smith",
"userPrincipalName":"john@contoso.com",
"mailNickname":"john",
"accountEnabled":true,
"passwordProfile":{"password":"P@ssw0rd!","forceChangePasswordNextSignIn":true}
}
Response example
{
"id": "def-456",
"displayName": "John Smith",
"userPrincipalName": "john@contoso.com",
"accountEnabled": true
}
Update user
- Method: PATCH
- URL:
https://graph.microsoft.com/v1.0/users/{id} - Watch out for: Synced (on-premises) users: most writeable attributes are blocked via Graph and must be updated in on-premises AD. Attempting to update returns a 403 or specific error.
Request example
PATCH /v1.0/users/def-456
Content-Type: application/json
{
"jobTitle": "Senior Engineer",
"department": "R&D"
}
Response example
HTTP 204 No Content
Delete user
- Method: DELETE
- URL:
https://graph.microsoft.com/v1.0/users/{id} - Watch out for: Deleted users are soft-deleted and moved to the deleted items container for 30 days before permanent deletion. Licenses are not automatically released immediately in all scenarios; verify license reclamation.
Request example
DELETE /v1.0/users/def-456
Authorization: Bearer {token}
Response example
HTTP 204 No Content
Assign license
- Method: POST
- URL:
https://graph.microsoft.com/v1.0/users/{id}/assignLicense - Watch out for: usageLocation must be set on the user before assigning a license, otherwise returns error. Retrieve available SKU IDs via GET /subscribedSkus.
Request example
POST /v1.0/users/def-456/assignLicense
Content-Type: application/json
{
"addLicenses":[{"skuId":"<sku-guid>","disabledPlans":[]}],
"removeLicenses":[]
}
Response example
{
"id": "def-456",
"assignedLicenses": [{"skuId":"<sku-guid>","disabledPlans":[]}]
}
Disable user (block sign-in)
- Method: PATCH
- URL:
https://graph.microsoft.com/v1.0/users/{id} - Watch out for: Disabling does not revoke existing active tokens immediately. Use revokeSignInSessions action for immediate session termination.
Request example
PATCH /v1.0/users/def-456
Content-Type: application/json
{
"accountEnabled": false
}
Response example
HTTP 204 No Content
Restore deleted user
- Method: POST
- URL:
https://graph.microsoft.com/v1.0/directory/deletedItems/{id}/restore - Watch out for: Restore is only possible within the 30-day soft-delete window. After 30 days the object is permanently deleted and unrecoverable via API.
Request example
POST /v1.0/directory/deletedItems/def-456/restore
Authorization: Bearer {token}
Response example
{
"id": "def-456",
"displayName": "John Smith",
"userPrincipalName": "john@contoso.com"
}
Rate limits, pagination, and events
- Rate limits: Microsoft Graph uses service-level throttling. Limits vary by resource and operation. When throttled, the API returns HTTP 429 with a Retry-After header. Per-app per-tenant limits apply; no single published global number.
- Rate-limit headers: Yes
- Retry-After header: Yes
- Rate-limit notes: Retry-After header is returned on HTTP 429 responses indicating seconds to wait. Microsoft recommends exponential backoff. Batch requests (up to 20 per batch) can reduce throttling risk. Some workloads (Exchange, SharePoint) have separate throttling policies.
- Pagination method: token
- Default page size: 100
- Max page size: 999
- Pagination pointer: $top (page size); @odata.nextLink (continuation token in response)
| Plan | Limit | Concurrent |
|---|---|---|
| All Microsoft 365 plans (Graph API) | ~10,000 requests per 10 minutes per app per tenant (general guidance); specific limits vary by workload (e.g., Users: 1,500 req/5 min per app per tenant for some operations) | 4 |
- Webhooks available: Yes
- Webhook notes: Microsoft Graph supports change notifications (webhooks) via subscriptions. Clients register a notification URL; Graph sends a POST to that URL when subscribed resources change. Subscriptions have a maximum expiry (varies by resource; users: up to 29 days) and must be renewed before expiry.
- Alternative event strategy: Microsoft Graph also supports change tracking via delta queries (GET /users/delta) for polling-based sync without webhooks.
- Webhook events: user created, user updated, user deleted, group membership changed, sign-in events (via Entra ID audit logs subscription)
SCIM API status
SCIM available: Yes
SCIM version: 2.0
Plan required: Microsoft Entra ID P1 or P2 (included in Microsoft 365 Business Premium, E3, E5; add-on for Business Basic/Standard)
Endpoint: Configured per enterprise application in Entra ID; format: https://graph.microsoft.com/scim/v2/ (for inbound provisioning) or tenant-specific endpoint generated in Entra provisioning configuration for outbound to third-party apps.
Supported operations: Create user (POST /Users), Read user (GET /Users/{id}), List users (GET /Users), Update user (PATCH /Users/{id}), Deactivate user (PATCH /Users/{id} with active:false), Delete user (DELETE /Users/{id}), Create group (POST /Groups), Update group membership (PATCH /Groups/{id})
Limitations:
- SCIM provisioning requires Entra ID P1 or P2 license; not available on Business Basic or Business Standard without the P1/P2 add-on.
- Inbound SCIM (provisioning into Entra ID from external IdP) is a separate configuration from outbound SCIM.
- Attribute mapping is configurable but some Entra ID attributes have no SCIM equivalent and require custom extension schemas.
- Provisioning cycles run approximately every 40 minutes; near-real-time is not guaranteed.
- On-premises AD-synced users may have limited writeback capability depending on Entra Connect configuration.
Common scenarios
Three scenarios cover the majority of lifecycle automation use cases:
Provision a new employee with license: POST /v1.0/users to create the account, then PATCH to set usageLocation before calling POST /v1.0/users/{id}/assignLicense. The usageLocation step is a hard prerequisite - the license assignment call returns an error if it is missing. Retrieve available skuIds from GET /v1.0/subscribedSkus before constructing the assignLicense payload. Mailbox creation after license assignment is asynchronous and may take several minutes.
Offboard a departing employee: PATCH /v1.0/users/{id} with accountEnabled:false blocks sign-in but does not revoke active sessions - POST /v1.0/users/{id}/revokeSignInSessions must be called separately for immediate token invalidation. Remove the license via POST /v1.0/users/{id}/assignLicense with the skuId in the removeLicenses array before calling DELETE to avoid license reclamation delays. DELETE is a soft delete; the user object moves to the deletedItems container and is recoverable for 30 days.
Delta query sync (polling): Initial call to GET /v1.0/users/delta with a $select projection returns all users and a deltaLink. Subsequent polls against the stored deltaLink return only changed or deleted users since the last sync. Delta tokens can expire if left unused for too long; an expired token requires a full re-sync. For lower-latency requirements, Graph change notification subscriptions (webhooks) are the alternative, but subscriptions on the users resource expire on a fixed schedule and must be renewed proactively - expired subscriptions stop delivering notifications silently.
Provision a new employee with license
- POST /v1.0/users with displayName, userPrincipalName, mailNickname, accountEnabled:true, passwordProfile.
- PATCH /v1.0/users/{id} to set usageLocation (e.g., 'US') if not set at creation.
- GET /v1.0/subscribedSkus to retrieve the skuId for the desired Microsoft 365 plan.
- POST /v1.0/users/{id}/assignLicense with the skuId in addLicenses array.
- Optionally PUT /v1.0/users/{id}/manager/$ref to assign a manager.
Watch out for: Steps must be in order: usageLocation must be set before assignLicense or the call returns an error. License provisioning (mailbox creation) may take several minutes after assignment.
Offboard a departing employee
- PATCH /v1.0/users/{id} with accountEnabled:false to block sign-in.
- POST /v1.0/users/{id}/revokeSignInSessions to invalidate existing tokens immediately.
- POST /v1.0/users/{id}/assignLicense with the skuId in removeLicenses array to reclaim the license.
- Optionally transfer mailbox data or set an Out-of-Office via Exchange Online APIs/PowerShell before deletion.
- DELETE /v1.0/users/{id} to soft-delete the user (recoverable for 30 days).
Watch out for: Disabling the account does not revoke active sessions; revokeSignInSessions must be called separately. Deleting without removing the license first may delay license reclamation.
Sync user changes via delta query (polling)
- Initial sync: GET /v1.0/users/delta?$select=id,displayName,userPrincipalName,accountEnabled to get all users and a deltaLink.
- Store the deltaLink returned in the final response page.
- On subsequent polls, GET the stored deltaLink URL to retrieve only changed/deleted users since last sync.
- Process @removed objects (deleted users) and updated property sets in the response.
- Store the new deltaLink for the next poll cycle.
Watch out for: Delta tokens can expire if not used for an extended period; if expired, restart with a full delta query. Delta query does not guarantee real-time delivery; use change notifications (webhooks) for lower latency requirements.
Why building this yourself is a trap
The most consequential API caveat is the on-premises sync boundary: users provisioned via Entra ID Connect have most writeable attributes locked at the Graph layer. PATCH calls against synced users return 403 or attribute-specific errors; the source of truth is on-premises Active Directory, and changes must originate there.
Any integration that does not detect and branch on the onPremisesSyncEnabled flag will produce silent partial updates against synced users.
SCIM provisioning to third-party apps via Entra ID requires P1 or P2 licensing - it is not available on Business Basic or Standard without the add-on. Provisioning cycles run approximately every 40 minutes; near-real-time delivery is not guaranteed, and inbound SCIM (external IdP to Entra ID) is a separate configuration from outbound.
Attribute mapping is configurable but some Entra ID fields have no SCIM equivalent and require custom extension schemas.
Throttling is service-level and workload-specific. The API returns HTTP 429 with a Retry-After header; Microsoft recommends exponential backoff. Batch requests (up to 20 per batch) reduce per-call overhead but do not bypass per-app-per-tenant throttle budgets.
The $select parameter should be used on every list and get call - without it, only a default subset of properties is returned, responses are unnecessarily large, and fields required for identity graph construction (department, jobTitle, usageLocation, assignedLicenses) may be absent from the response.
Automate Microsoft 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.