The delta query in Microsoft Graph lets you query for additions, deletions, or updates to supported resources, 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 does an initial full synchronization of all the groups in a tenant, and then gets incremental changes to groups periodically.
Track changes to groups
Track user changes through one or more GET requests with the delta function. The GET request has the following characteristics:
The delta function prepended to the URL path.
A state token (deltatoken or skiptoken) from the previous GET delta function call.
[Optional] Any supported query parameters
Example
This article shows a series of example requests to track changes to groups:
To track changes in the group resource, make a request and include the delta function as a URL segment.
Tip
/delta is a shortcut for the fully qualified name /microsoft.graph.delta. Requests generated by Microsoft Graph SDKs use the fully qualified name.
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. If required, query parameters must be specified in the initial request.
Only properties included in $select are tracked for changes. If $select isn't specified, all properties of the object are tracked for changes.
The optional $select query parameter is also used to show how group members can be retrieved together with group objects. This capability 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 are 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.GetAsDeltaGetResponseAsync((requestConfiguration) =>
{
requestConfiguration.QueryParameters.Select = new string []{ "displayName","description","members" };
});
// Code snippets are only available for the latest major version. Current major version is $v1.*
// Dependencies
import (
"context"
msgraphsdk "github.com/microsoftgraph/msgraph-sdk-go"
graphgroups "github.com/microsoftgraph/msgraph-sdk-go/groups"
//other-imports
)
requestParameters := &graphgroups.GroupsDeltaRequestBuilderGetQueryParameters{
Select: [] string {"displayName","description","members"},
}
configuration := &graphgroups.GroupsDeltaRequestBuilderGetRequestConfiguration{
QueryParameters: requestParameters,
}
// To initialize your graphClient, see https://learn.microsoft.com/en-us/graph/sdks/create-client?from=snippets&tabs=go
delta, err := graphClient.Groups().Delta().GetAsDeltaGetResponse(context.Background(), configuration)
// Code snippets are only available for the latest version. Current version is 6.x
GraphServiceClient graphClient = new GraphServiceClient(requestAdapter);
var result = graphClient.groups().delta().get(requestConfiguration -> {
requestConfiguration.queryParameters.select = new String []{"displayName", "description", "members"};
});
# Code snippets are only available for the latest version. Current version is 1.x
from msgraph import GraphServiceClient
from msgraph.generated.groups.delta.delta_request_builder import DeltaRequestBuilder
from kiota_abstractions.base_request_configuration import RequestConfiguration
# To initialize your graph_client, see https://learn.microsoft.com/en-us/graph/sdks/create-client?from=snippets&tabs=python
query_params = DeltaRequestBuilder.DeltaRequestBuilderGetQueryParameters(
select = ["displayName","description","members"],
)
request_configuration = RequestConfiguration(
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 is included.
In this example, a @odata.nextLink URL is returned indicating there are more pages of data to be retrieved in the session. Notice the $skiptoken in the URL. The $select query parameter from the initial request is encoded into the @odata.nextLink URL.
The members@delta property is included in the All Company group and contains the two current members of the group. sg-HR doesn't contain that property because the group doesn't have any members.
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"
}
]
}
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
// Note: The URI string parameter used with WithUrl() can be retrieved using the OdataNextLink or OdataDeltaLink property from a response object
var result = await graphClient.Groups.Delta.WithUrl("https://graph.microsoft.com/v1.0/groups/delta?$skiptoken=pqwSUjGYvb3jQpbwVAwEL7yuI3dU1LecfkkfLPtnIjvB7XnF_yllFsCrZJ").GetAsDeltaGetResponseAsync();
// Code snippets are only available for the latest major version. Current major version is $v1.*
// Dependencies
import (
"context"
msgraphsdk "github.com/microsoftgraph/msgraph-sdk-go"
graphgroups "github.com/microsoftgraph/msgraph-sdk-go/groups"
//other-imports
)
requestSkiptoken := "pqwSUjGYvb3jQpbwVAwEL7yuI3dU1LecfkkfLPtnIjvB7XnF_yllFsCrZJ"
requestParameters := &graphgroups.GroupsDeltaRequestBuilderGetQueryParameters{
Skiptoken: &requestSkiptoken,
}
configuration := &graphgroups.GroupsDeltaRequestBuilderGetRequestConfiguration{
QueryParameters: requestParameters,
}
// To initialize your graphClient, see https://learn.microsoft.com/en-us/graph/sdks/create-client?from=snippets&tabs=go
delta, err := graphClient.Groups().Delta().GetAsDeltaGetResponse(context.Background(), configuration)
// Code snippets are only available for the latest version. Current version is 6.x
GraphServiceClient graphClient = new GraphServiceClient(requestAdapter);
com.microsoft.graph.groups.delta.DeltaRequestBuilder deltaRequestBuilder = new com.microsoft.graph.groups.delta.DeltaRequestBuilder("https://graph.microsoft.com/v1.0/groups/delta?$skiptoken=pqwSUjGYvb3jQpbwVAwEL7yuI3dU1LecfkkfLPtnIjvB7XnF_yllFsCrZJ", graphClient.getRequestAdapter());
com.microsoft.graph.groups.delta.DeltaGetResponse result = deltaRequestBuilder.get();
# Code snippets are only available for the latest version. Current version is 1.x
from msgraph import GraphServiceClient
from msgraph.generated.groups.delta.delta_request_builder import DeltaRequestBuilder
from kiota_abstractions.base_request_configuration import RequestConfiguration
# To initialize your graph_client, see https://learn.microsoft.com/en-us/graph/sdks/create-client?from=snippets&tabs=python
query_params = DeltaRequestBuilder.DeltaRequestBuilderGetQueryParameters(
skiptoken = "pqwSUjGYvb3jQpbwVAwEL7yuI3dU1LecfkkfLPtnIjvB7XnF_yllFsCrZJ",
)
request_configuration = RequestConfiguration(
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 subsequent 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
// Note: The URI string parameter used with WithUrl() can be retrieved using the OdataNextLink or OdataDeltaLink property from a response object
var result = await graphClient.Groups.Delta.WithUrl("https://graph.microsoft.com/v1.0/groups/delta?$skiptoken=ppqwSUjGYvb3jQpbwVAwEL7yuI3dU1LecfkkfLPtnIjtQ5LOhVoS7qQG_wdVCHHlbQpga7").GetAsDeltaGetResponseAsync();
// Code snippets are only available for the latest major version. Current major version is $v1.*
// Dependencies
import (
"context"
msgraphsdk "github.com/microsoftgraph/msgraph-sdk-go"
graphgroups "github.com/microsoftgraph/msgraph-sdk-go/groups"
//other-imports
)
requestSkiptoken := "ppqwSUjGYvb3jQpbwVAwEL7yuI3dU1LecfkkfLPtnIjtQ5LOhVoS7qQG_wdVCHHlbQpga7"
requestParameters := &graphgroups.GroupsDeltaRequestBuilderGetQueryParameters{
Skiptoken: &requestSkiptoken,
}
configuration := &graphgroups.GroupsDeltaRequestBuilderGetRequestConfiguration{
QueryParameters: requestParameters,
}
// To initialize your graphClient, see https://learn.microsoft.com/en-us/graph/sdks/create-client?from=snippets&tabs=go
delta, err := graphClient.Groups().Delta().GetAsDeltaGetResponse(context.Background(), configuration)
// Code snippets are only available for the latest version. Current version is 6.x
GraphServiceClient graphClient = new GraphServiceClient(requestAdapter);
com.microsoft.graph.groups.delta.DeltaRequestBuilder deltaRequestBuilder = new com.microsoft.graph.groups.delta.DeltaRequestBuilder("https://graph.microsoft.com/v1.0/groups/delta?$skiptoken=ppqwSUjGYvb3jQpbwVAwEL7yuI3dU1LecfkkfLPtnIjtQ5LOhVoS7qQG_wdVCHHlbQpga7", graphClient.getRequestAdapter());
com.microsoft.graph.groups.delta.DeltaGetResponse result = deltaRequestBuilder.get();
# Code snippets are only available for the latest version. Current version is 1.x
from msgraph import GraphServiceClient
from msgraph.generated.groups.delta.delta_request_builder import DeltaRequestBuilder
from kiota_abstractions.base_request_configuration import RequestConfiguration
# To initialize your graph_client, see https://learn.microsoft.com/en-us/graph/sdks/create-client?from=snippets&tabs=python
query_params = DeltaRequestBuilder.DeltaRequestBuilderGetQueryParameters(
skiptoken = "ppqwSUjGYvb3jQpbwVAwEL7yuI3dU1LecfkkfLPtnIjtQ5LOhVoS7qQG_wdVCHHlbQpga7",
)
request_configuration = RequestConfiguration(
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 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 tracked property changed (for example, an updated displayName).
Group objects for which member objects were 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
// Note: The URI string parameter used with WithUrl() can be retrieved using the OdataNextLink or OdataDeltaLink property from a response object
var result = await graphClient.Groups.Delta.WithUrl("https://graph.microsoft.com/v1.0/groups/delta?$deltatoken=sZwAFZibx-LQOdZIo1hHhmmDhHzCY0Hs6snoIHJCSIfCHdqKdWNZ2VX3kErpyna9GygROwBk-rqWWMFxJC3pw").GetAsDeltaGetResponseAsync();
// Code snippets are only available for the latest major version. Current major version is $v1.*
// Dependencies
import (
"context"
msgraphsdk "github.com/microsoftgraph/msgraph-sdk-go"
graphgroups "github.com/microsoftgraph/msgraph-sdk-go/groups"
//other-imports
)
requestDeltatoken := "sZwAFZibx-LQOdZIo1hHhmmDhHzCY0Hs6snoIHJCSIfCHdqKdWNZ2VX3kErpyna9GygROwBk-rqWWMFxJC3pw"
requestParameters := &graphgroups.GroupsDeltaRequestBuilderGetQueryParameters{
Deltatoken: &requestDeltatoken,
}
configuration := &graphgroups.GroupsDeltaRequestBuilderGetRequestConfiguration{
QueryParameters: requestParameters,
}
// To initialize your graphClient, see https://learn.microsoft.com/en-us/graph/sdks/create-client?from=snippets&tabs=go
delta, err := graphClient.Groups().Delta().GetAsDeltaGetResponse(context.Background(), configuration)
// Code snippets are only available for the latest version. Current version is 6.x
GraphServiceClient graphClient = new GraphServiceClient(requestAdapter);
com.microsoft.graph.groups.delta.DeltaRequestBuilder deltaRequestBuilder = new com.microsoft.graph.groups.delta.DeltaRequestBuilder("https://graph.microsoft.com/v1.0/groups/delta?$deltatoken=sZwAFZibx-LQOdZIo1hHhmmDhHzCY0Hs6snoIHJCSIfCHdqKdWNZ2VX3kErpyna9GygROwBk-rqWWMFxJC3pw", graphClient.getRequestAdapter());
com.microsoft.graph.groups.delta.DeltaGetResponse result = deltaRequestBuilder.get();
# Code snippets are only available for the latest version. Current version is 1.x
from msgraph import GraphServiceClient
from msgraph.generated.groups.delta.delta_request_builder import DeltaRequestBuilder
from kiota_abstractions.base_request_configuration import RequestConfiguration
# To initialize your graph_client, see https://learn.microsoft.com/en-us/graph/sdks/create-client?from=snippets&tabs=python
query_params = DeltaRequestBuilder.DeltaRequestBuilderGetQueryParameters(
deltatoken = "sZwAFZibx-LQOdZIo1hHhmmDhHzCY0Hs6snoIHJCSIfCHdqKdWNZ2VX3kErpyna9GygROwBk-rqWWMFxJC3pw",
)
request_configuration = RequestConfiguration(
query_parameters = query_params,
)
result = await graph_client.groups.delta.get(request_configuration = request_configuration)
If there are no changes, 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 there are changes, 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.
Some things to note about the 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 - 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 user with ID 632f6bb2-3ec8-4c1f-9073-0027a8c6859 was removed from the group by removing their membership, as described by the @removed property. Objects that are removed from a group through deletion of the object aren't detected by delta queries.
The second user with ID 37de1ae3-408f-4702-8636-20824abda004 was added to the group.
A group object can contain the @removed annotation in the following scenarios:
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.
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"
}
]
}
]
}
Paging through members in a large group
The members@delta property is included in group objects by default, when the $select query parameter isn't 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.GetAsDeltaGetResponseAsync((requestConfiguration) =>
{
requestConfiguration.QueryParameters.Select = new string []{ "displayName","description","members" };
});
// Code snippets are only available for the latest major version. Current major version is $v1.*
// Dependencies
import (
"context"
msgraphsdk "github.com/microsoftgraph/msgraph-sdk-go"
graphgroups "github.com/microsoftgraph/msgraph-sdk-go/groups"
//other-imports
)
requestParameters := &graphgroups.GroupsDeltaRequestBuilderGetQueryParameters{
Select: [] string {"displayName","description","members"},
}
configuration := &graphgroups.GroupsDeltaRequestBuilderGetRequestConfiguration{
QueryParameters: requestParameters,
}
// To initialize your graphClient, see https://learn.microsoft.com/en-us/graph/sdks/create-client?from=snippets&tabs=go
delta, err := graphClient.Groups().Delta().GetAsDeltaGetResponse(context.Background(), configuration)
// Code snippets are only available for the latest version. Current version is 6.x
GraphServiceClient graphClient = new GraphServiceClient(requestAdapter);
var result = graphClient.groups().delta().get(requestConfiguration -> {
requestConfiguration.queryParameters.select = new String []{"displayName", "description", "members"};
});
# Code snippets are only available for the latest version. Current version is 1.x
from msgraph import GraphServiceClient
from msgraph.generated.groups.delta.delta_request_builder import DeltaRequestBuilder
from kiota_abstractions.base_request_configuration import RequestConfiguration
# To initialize your graph_client, see https://learn.microsoft.com/en-us/graph/sdks/create-client?from=snippets&tabs=python
query_params = DeltaRequestBuilder.DeltaRequestBuilderGetQueryParameters(
select = ["displayName","description","members"],
)
request_configuration = RequestConfiguration(
query_parameters = query_params,
)
result = await graph_client.groups.delta.get(request_configuration = request_configuration)
Microsoft Graph might 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 might receive a response containing the same group object. The same property values are 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 is returned in this fashion, and other groups 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.