The delta query in Microsoft Graph lets you query for additions, deletions, or updates to supported resources. It's enabled through a series of delta requests. For groups, the delta query enables you to discover changes without fetching the entire set of groups to compare changes.
Clients that synchronize groups with a local profile store can use the delta query for both their initial full synchronization along with subsequent incremental synchronizations. Typically, a client would do an initial full synchronization of all the groups in a tenant, and then, get incremental changes to groups periodically.
Track changes to groups
Track groups changes through one or more GET requests with the delta function. The GET request is like a list groups request, except with the following extra objects in the URL:
The delta function.
A state token (deltaToken or skipToken) from the previous GET delta function call.
Example: track changes to groups
The following example shows a series of requests to track changes to groups:
When a group is deleted (Microsoft 365 groups), the item contains an annotation: @removed with value of "reason": "changed".
When the group is permanently deleted (a security group or permanently deleting a Microsoft 365 group), the item contains an annotation: @removed with value of "reason": "deleted".
When the group is created, or restored, there's no annotation.
Initial request
To track changes in the group resource, make a request and include the delta function as a URL segment.
Take note of the following items:
The optional $select query parameter is included in the request to demonstrate how query parameters are automatically included in future requests.
The optional $select query parameter is also used to show how group members can be retrieved together with group objects. This allows tracking of membership changes, such as when users are added or removed from groups.
The initial request doesn't include a state token. State tokens will be used in subsequent requests.
GET https://graph.microsoft.com/v1.0/groups/delta?$select=displayName,description,members
// Code snippets are only available for the latest version. Current version is 5.x
// To initialize your graphClient, see https://learn.microsoft.com/en-us/graph/sdks/create-client?from=snippets&tabs=csharp
var result = await graphClient.Groups.Delta.GetAsync((requestConfiguration) =>
{
requestConfiguration.QueryParameters.Select = new string []{ "displayName","description","members" };
});
<?php
// THIS SNIPPET IS A PREVIEW VERSION OF THE SDK. NON-PRODUCTION USE ONLY
$graphServiceClient = new GraphServiceClient($tokenRequestContext, $scopes);
$requestConfiguration = new DeltaRequestBuilderGetRequestConfiguration();
$queryParameters = DeltaRequestBuilderGetRequestConfiguration::createQueryParameters();
$queryParameters->select = ["displayName","description","members"];
$requestConfiguration->queryParameters = $queryParameters;
$result = $graphServiceClient->groups()->delta()->get($requestConfiguration)->wait();
# THE PYTHON SDK IS IN PREVIEW. FOR NON-PRODUCTION USE ONLY
graph_client = GraphServiceClient(credentials, scopes)
query_params = DeltaRequestBuilder.DeltaRequestBuilderGetQueryParameters(
select = ["displayName","description","members"],
)
request_configuration = DeltaRequestBuilder.DeltaRequestBuilderGetRequestConfiguration(
query_parameters = query_params,
)
result = await graph_client.groups.delta.get(request_configuration = request_configuration)
If successful, this method returns 200 OK response code and group collection object in the response body. If the entire set of groups is too large to fit in one response, a @odata.nextLink containing a state token will also be included.
In this example, a @odata.nextLink was included; the original $select query parameter is encoded in the state token.
HTTP/1.1 200 OK
Content-type: application/json
{
"@odata.context":"https://graph.microsoft.com/v1.0/$metadata#groups(displayName,description)",
"@odata.nextLink":"https://graph.microsoft.com/v1.0/groups/delta?$skiptoken=pqwSUjGYvb3jQpbwVAwEL7yuI3dU1LecfkkfLPtnIjvB7XnF_yllFsCrZJ",
"value": [
{
"displayName":"All Company",
"description":"This is the default group for everyone in the network",
"id":"c2f798fd-f95d-4623-8824-63aec21fffff",
"members@delta": [
{
"@odata.type": "#microsoft.graph.user",
"id": "693acd06-2877-4339-8ade-b704261fe7a0"
},
{
"@odata.type": "#microsoft.graph.user",
"id": "49320844-be99-4164-8167-87ff5d047ace"
}
]
},
{
"displayName":"sg-HR",
"description":"All HR personnel",
"id":"ec22655c-8eb2-432a-b4ea-8b8a254bffff"
}
]
}
Note
The members@delta property is included in the first group object - All Company - and contains the two current members of the group. sg-HR does not contain that property because the group does not have any members.
nextLink request
The second request uses the @odata.nextLink from the previous response, which contains the skipToken. Notice the $select parameter isn't visibly present as it's encoded and included in the token.
GET https://graph.microsoft.com/v1.0/groups/delta?$skiptoken=pqwSUjGYvb3jQpbwVAwEL7yuI3dU1LecfkkfLPtnIjvB7XnF_yllFsCrZJ
// Code snippets are only available for the latest version. Current version is 5.x
// To initialize your graphClient, see https://learn.microsoft.com/en-us/graph/sdks/create-client?from=snippets&tabs=csharp
var deltaRequestBuilder = new Microsoft.Graph.Groups.Delta.DeltaRequestBuilder("https://graph.microsoft.com/v1.0/groups/delta?$skiptoken=pqwSUjGYvb3jQpbwVAwEL7yuI3dU1LecfkkfLPtnIjvB7XnF_yllFsCrZJ", graphClient.RequestAdapter);
var result = await deltaRequestBuilder.GetAsync();
<?php
// THIS SNIPPET IS A PREVIEW VERSION OF THE SDK. NON-PRODUCTION USE ONLY
$graphServiceClient = new GraphServiceClient($tokenRequestContext, $scopes);
$requestConfiguration = new DeltaRequestBuilderGetRequestConfiguration();
$queryParameters = DeltaRequestBuilderGetRequestConfiguration::createQueryParameters();
$queryParameters->skiptoken = "pqwSUjGYvb3jQpbwVAwEL7yuI3dU1LecfkkfLPtnIjvB7XnF_yllFsCrZJ";
$requestConfiguration->queryParameters = $queryParameters;
$result = $graphServiceClient->groups()->delta()->get($requestConfiguration)->wait();
# THE PYTHON SDK IS IN PREVIEW. FOR NON-PRODUCTION USE ONLY
graph_client = GraphServiceClient(credentials, scopes)
query_params = DeltaRequestBuilder.DeltaRequestBuilderGetQueryParameters(
skiptoken = "pqwSUjGYvb3jQpbwVAwEL7yuI3dU1LecfkkfLPtnIjvB7XnF_yllFsCrZJ",
)
request_configuration = DeltaRequestBuilder.DeltaRequestBuilderGetRequestConfiguration(
query_parameters = query_params,
)
result = await graph_client.groups.delta.get(request_configuration = request_configuration)
The response contains another @odata.nextLink with a new skipToken value, which indicates that more changes that were tracked for groups are available. Use the @odata.nextLink URL in more requests until a @odata.deltaLink URL (in an @odata.deltaLink parameter) is returned in the final response, even if the value is an empty array.
GET https://graph.microsoft.com/v1.0/groups/delta?$skiptoken=ppqwSUjGYvb3jQpbwVAwEL7yuI3dU1LecfkkfLPtnIjtQ5LOhVoS7qQG_wdVCHHlbQpga7
// Code snippets are only available for the latest version. Current version is 5.x
// To initialize your graphClient, see https://learn.microsoft.com/en-us/graph/sdks/create-client?from=snippets&tabs=csharp
var deltaRequestBuilder = new Microsoft.Graph.Groups.Delta.DeltaRequestBuilder("https://graph.microsoft.com/v1.0/groups/delta?$skiptoken=ppqwSUjGYvb3jQpbwVAwEL7yuI3dU1LecfkkfLPtnIjtQ5LOhVoS7qQG_wdVCHHlbQpga7", graphClient.RequestAdapter);
var result = await deltaRequestBuilder.GetAsync();
<?php
// THIS SNIPPET IS A PREVIEW VERSION OF THE SDK. NON-PRODUCTION USE ONLY
$graphServiceClient = new GraphServiceClient($tokenRequestContext, $scopes);
$requestConfiguration = new DeltaRequestBuilderGetRequestConfiguration();
$queryParameters = DeltaRequestBuilderGetRequestConfiguration::createQueryParameters();
$queryParameters->skiptoken = "ppqwSUjGYvb3jQpbwVAwEL7yuI3dU1LecfkkfLPtnIjtQ5LOhVoS7qQG_wdVCHHlbQpga7";
$requestConfiguration->queryParameters = $queryParameters;
$result = $graphServiceClient->groups()->delta()->get($requestConfiguration)->wait();
# THE PYTHON SDK IS IN PREVIEW. FOR NON-PRODUCTION USE ONLY
graph_client = GraphServiceClient(credentials, scopes)
query_params = DeltaRequestBuilder.DeltaRequestBuilderGetQueryParameters(
skiptoken = "ppqwSUjGYvb3jQpbwVAwEL7yuI3dU1LecfkkfLPtnIjtQ5LOhVoS7qQG_wdVCHHlbQpga7",
)
request_configuration = DeltaRequestBuilder.DeltaRequestBuilderGetRequestConfiguration(
query_parameters = query_params,
)
result = await graph_client.groups.delta.get(request_configuration = request_configuration)
When a @odata.deltaLink URL is returned, there's no more data about the existing state of group objects. For future requests, the application uses the @odata.deltaLink URL to learn about other changes to groups. Save the deltaToken and use it in the subsequent request URL to discover more changes to groups.
Using the @odata.deltaLink from the last response, you'll get changes (additions, deletions, or updates) to groups since the last request. Changes include:
Newly created group objects.
Deleted group objects.
Group objects for which a property has changed (for example, displayName has been modified).
Group objects for which member objects have been added or removed.
GET https://graph.microsoft.com/v1.0/groups/delta?$deltatoken=sZwAFZibx-LQOdZIo1hHhmmDhHzCY0Hs6snoIHJCSIfCHdqKdWNZ2VX3kErpyna9GygROwBk-rqWWMFxJC3pw
// Code snippets are only available for the latest version. Current version is 5.x
// To initialize your graphClient, see https://learn.microsoft.com/en-us/graph/sdks/create-client?from=snippets&tabs=csharp
var deltaRequestBuilder = new Microsoft.Graph.Groups.Delta.DeltaRequestBuilder("https://graph.microsoft.com/v1.0/groups/delta?$deltatoken=sZwAFZibx-LQOdZIo1hHhmmDhHzCY0Hs6snoIHJCSIfCHdqKdWNZ2VX3kErpyna9GygROwBk-rqWWMFxJC3pw", graphClient.RequestAdapter);
var result = await deltaRequestBuilder.GetAsync();
<?php
// THIS SNIPPET IS A PREVIEW VERSION OF THE SDK. NON-PRODUCTION USE ONLY
$graphServiceClient = new GraphServiceClient($tokenRequestContext, $scopes);
$requestConfiguration = new DeltaRequestBuilderGetRequestConfiguration();
$queryParameters = DeltaRequestBuilderGetRequestConfiguration::createQueryParameters();
$queryParameters->deltatoken = "sZwAFZibx-LQOdZIo1hHhmmDhHzCY0Hs6snoIHJCSIfCHdqKdWNZ2VX3kErpyna9GygROwBk-rqWWMFxJC3pw";
$requestConfiguration->queryParameters = $queryParameters;
$result = $graphServiceClient->groups()->delta()->get($requestConfiguration)->wait();
# THE PYTHON SDK IS IN PREVIEW. FOR NON-PRODUCTION USE ONLY
graph_client = GraphServiceClient(credentials, scopes)
query_params = DeltaRequestBuilder.DeltaRequestBuilderGetQueryParameters(
deltatoken = "sZwAFZibx-LQOdZIo1hHhmmDhHzCY0Hs6snoIHJCSIfCHdqKdWNZ2VX3kErpyna9GygROwBk-rqWWMFxJC3pw",
)
request_configuration = DeltaRequestBuilder.DeltaRequestBuilderGetRequestConfiguration(
query_parameters = query_params,
)
result = await graph_client.groups.delta.get(request_configuration = request_configuration)
If no changes have occurred, a @odata.deltaLink is returned with no results - the value property is an empty array. Make sure to replace the previous link in the application with the new one for use in future calls.
If changes have occurred, a collection of changed groups is included. The response also contains either a @odata.nextLink - in case there are multiple pages of changes to retrieve - or a @odata.deltaLink. Implement the same pattern of following the @odata.nextLink and persist the final @odata.deltaLink for future calls.
Note
This request might have replication delays for groups that were recently created, updated, or deleted. Retry the @odata.nextLink or @odata.deltaLink after some time to retrieve the latest changes.
HTTP/1.1 200 OK
Content-type: application/json
{
"@odata.context":"https://graph.microsoft.com/v1.0/$metadata#groups",
"@odata.deltaLink":"https://graph.microsoft.com/v1.0/groups/delta?$deltatoken=sZwAFZibx-LQOdZIo1hHhmmDhHzCY0Hs6snoIHJCSIfCHdqKdWNZ2VX3kErpyna9GygROwBk-rqWWMFxJC3pw",
"value": [
{
"displayName": "TestGroup3",
"description": "A test group for change tracking",
"id": "2e5807ce-58f3-4a94-9b37-ffff2e085957",
"members@delta": [
{
"@odata.type": "#microsoft.graph.user",
"id": "632f6bb2-3ec8-4c1f-9073-0027a8c6859",
"@removed": {
"reason": "deleted"
}
},
{
"@odata.type": "#microsoft.graph.user",
"id": "37de1ae3-408f-4702-8636-20824abda004"
}
]
}
]
}
Some things to note about the previous example response:
The objects are returned with the same set of properties originally specified via the $select query parameter.
Both changed and unchanged properties are included. In the example above, the description property has a new value, while the displayName property hasn't changed.
members@delta contains the following changes to the group membership.
The first user in the list has been removed from the group - either by removing the membership or by deleting the user object itself. The @removed property describes that. Only users that have been permanently deleted are removed from groups. Users that have been temporary deleted keep their group memberships and won't appear in the delta result until they're permanently deleted. For details, see directory (deleted items).
The second user has been added to the group.
Paging through members in a large group
The members@delta property is included in group objects by default, when the $select query parameter hasn't been specified, or when the $select=members parameter is explicitly specified. For groups with many members, it's possible that all members can't fit into a single response. Implement the following pattern to handle such cases.
Note
This pattern applies to both the initial retrieval of group state as well as to subsequent calls to get delta changes.
Let's assume you're running the following delta query - either to capture the initial full state of groups, or later on to get delta changes:
GET https://graph.microsoft.com/v1.0/groups/delta?$select=displayName,description,members
// Code snippets are only available for the latest version. Current version is 5.x
// To initialize your graphClient, see https://learn.microsoft.com/en-us/graph/sdks/create-client?from=snippets&tabs=csharp
var result = await graphClient.Groups.Delta.GetAsync((requestConfiguration) =>
{
requestConfiguration.QueryParameters.Select = new string []{ "displayName","description","members" };
});
<?php
// THIS SNIPPET IS A PREVIEW VERSION OF THE SDK. NON-PRODUCTION USE ONLY
$graphServiceClient = new GraphServiceClient($tokenRequestContext, $scopes);
$requestConfiguration = new DeltaRequestBuilderGetRequestConfiguration();
$queryParameters = DeltaRequestBuilderGetRequestConfiguration::createQueryParameters();
$queryParameters->select = ["displayName","description","members"];
$requestConfiguration->queryParameters = $queryParameters;
$result = $graphServiceClient->groups()->delta()->get($requestConfiguration)->wait();
# THE PYTHON SDK IS IN PREVIEW. FOR NON-PRODUCTION USE ONLY
graph_client = GraphServiceClient(credentials, scopes)
query_params = DeltaRequestBuilder.DeltaRequestBuilderGetQueryParameters(
select = ["displayName","description","members"],
)
request_configuration = DeltaRequestBuilder.DeltaRequestBuilderGetRequestConfiguration(
query_parameters = query_params,
)
result = await graph_client.groups.delta.get(request_configuration = request_configuration)
Microsoft Graph may return a response that contains just one group object, with a large list of members in the members@delta property:
First page
HTTP/1.1 200 OK
Content-type: application/json
{
"@odata.context":"https://graph.microsoft.com/v1.0/$metadata#groups",
"@odata.nextLink":"https://graph.microsoft.com/v1.0/groups/delta?$skiptoken=<...>",
"value": [
{
"displayName":"LargeGroup",
"description":"A group containing thousands of users",
"id":"2e5807ce-58f3-4a94-9b37-ffff2e085957",
"members@delta": [
{
"@odata.type": "#microsoft.graph.user",
"id": "632f6bb2-3ec8-4c1f-9073-0027a8c6859",
"@removed": {
"reason": "deleted"
}
},
{
"@odata.type": "#microsoft.graph.user",
"id": "37de1ae3-408f-4702-8636-20824abda004"
},
<...more users here...>
]
}
<...no more groups included - this group filled out the entire response...>
]
}
When you follow the @odata.nextLink, you may receive a response containing the same group object. The same property values will be returned but the members@delta property now contains a different list of users.
Second page
HTTP/1.1 200 OK
Content-type: application/json
{
"@odata.context":"https://graph.microsoft.com/v1.0/$metadata#groups",
"@odata.nextLink":"https://graph.microsoft.com/v1.0/groups/delta?$skiptoken=<...>",
"value": [
{
"displayName":"LargeGroup",
"description":"A group containing thousands of users",
"id":"2e5807ce-58f3-4a94-9b37-ffff2e085957",
"members@delta": [
{
"@odata.type": "#microsoft.graph.user",
"id": "c08a463b-7b8a-40a4-aa31-f9bf690b9551",
"@removed": {
"reason": "deleted"
}
},
{
"@odata.type": "#microsoft.graph.user",
"id": "23423fa6-821e-44b2-aae4-d039d33884c2"
},
<...more users here...>
]
}
<...no more groups included - this group filled out the entire response...>
]
}
Eventually, the entire member list will be returned in this fashion, and other groups will start showing up in the response.
We recommend the following best practices to correctly handle this pattern:
Always follow @odata.nextLink and locally merge each group's state: as you receive responses related to the same group, use them to build the full membership list in your application.
Don't assume a specific sequence of the responses. Assume that the same group could show up anywhere in the @odata.nextLink sequence and handle that in your merge logic.