Introduction
Changed Elements API V3 creates asynchronous diff jobs for a range of iModel changesets. This tutorial shows how to create and customize those jobs so that the results contain only the change data that your workflow needs.
The examples below demonstrate how to create, monitor, and delete diff jobs, as well as how to configure custom processing pipelines. The examples use placeholder GUIDs for iTwinId and iModelId.
Pre-requisites
- All requests require you to pass an
Authorizationheader with a valid access token. - You must have access to your own iTwin and iModel.
- Use an HTTP REST client of your choice, such as Postman or curl.
Info
Skill level:
Basic
Duration:
25 minutes
How to use the API
Here's the typical workflow for using the Changed Elements API V3:
POST /diffreturns a job in theQueuedstate.GET /diff/{id}?iTwinId={iTwinId}&iModelId={iModelId}to poll the job resource until it isCompleted. (Alternatively, use webhooks to be notified when the job completes. This is out of scope for this tutorial.)- Fetch the
hrefURL to download the result JSON blob.
In the examples in this tutorial, replace the placeholder GUIDs with your own iTwinId and iModelId, and adjust the changeset range to the one you'd like to inspect.
Note: V3 accepts changeset indices (positive integers) and changeset ids (40-character hashes). We will use indices for simplicity.
Create a diff job
POST https://api.bentley.com/changedelements/diff
Authorization: Bearer <token>
Accept: application/vnd.bentley.itwin-platform.v3+json
{
"iTwinId": "11111111-1111-1111-1111-111111111111",
"iModelId": "11111111-1111-1111-1111-111111111111",
"startChangeset": 12,
"endChangeset": 18,
"diffingPlan": {
"pipeline": [
{ "name": "compute-changes" }
]
}
}Poll until completed and download HREF
GET https://api.bentley.com/changedelements/diff/3fa85f64-5717-4562-b3fc-2c963f66afa6?iTwinId=11111111-1111-1111-1111-111111111111&iModelId=11111111-1111-1111-1111-111111111111 HTTP/1.1
Authorization: Bearer <token>
Accept: application/vnd.bentley.itwin-platform.v3+json
HTTP/1.1 200 OK
{
"job": {
"jobId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"status": "Completed",
"href": "https://example.blob.core.windows.net/results.json?sv=..."
}
}
GET https://example.blob.core.windows.net/results.json?sv=... HTTP/1.1Delete a diff job when it is no longer needed
DELETE https://api.bentley.com/changedelements/diff/3fa85f64-5717-4562-b3fc-2c963f66afa6?iTwinId=11111111-1111-1111-1111-111111111111&iModelId=11111111-1111-1111-1111-111111111111 HTTP/1.1
Authorization: Bearer <token>
Accept: application/vnd.bentley.itwin-platform.v3+json
HTTP/1.1 204 No ContentPipeline steps primer: configuring your diff
Now that we've covered the basics, let's dive into configuring your own diffing pipelines to cater to your specific needs.
A custom diffingPlan contains a pipeline array. Every pipeline needs a compute-changes step: it is the source step that reads and computes the changes over the requested changeset range. You may list it explicitly, as this tutorial does, or it will be inserted automatically as a prerequisite to another step. That is why it always appears in a resolved pipeline JSON.
Here's a list of useful steps you can use in your configuration:
drop-duplicate-updates: keeps only the new record for updates.drop-non-elements: removes relationships and other non-elements.filter-fields: keeps or removes fields.add-class-full-name: queries for and adds the full class name to each element.add-model-id: queries for and adds the model id to each element.
See the full documentation for more details.
The optional output object controls the final result, including id encoding, selected output fields, and field names.
Predefined Basic, Full, and VersionCompare strategies are shortcuts for common workflows. They are useful starting points, but this tutorial focuses on custom pipelines. A strategy can also be used as one step inside a pipeline when you want to extend it.
Diffing plan example
{
// ...
"diffingPlan": {
"pipeline": [
{ "name": "compute-changes" },
{ "name": "drop-duplicate-updates" },
{ "name": "filter-fields", "config": { /* ... */ } },
{ "name": "add-class-full-name" },
{ "name": "add-model-id" }
]
}
}Output configuration primer: shaping the final result
While pipeline steps transform the change data as it is processed, the optional output object controls how the final records are written to the result blob. It runs after the last pipeline step.
Here are the options you can use:
format: overall shape of the payload.basicproduces minimal{ id, classFullName, operation }records;versionCompareproduces the columnar Version Compare shape. Omit it to return records exactly as the pipeline produced them.idEncoding: how element ids are encoded —hex(0x..., the default) ordecimal.keepFields: allow-list of fields to retain in the final records.dropFields: deny-list of fields to remove. Mutually exclusive withkeepFields— provide at most one.renameFields: maps an original field name to a new output name. Applied last, after fields are kept or dropped.
Note that the element id field is always preserved, even when not listed in keepFields. When a strategy already defines a default output (for example, Basic), providing your own output replaces that default entirely.
The examples that follow use these options progressively: Example 1 omits output for raw passthrough, Example 2 applies format and idEncoding, and Example 3 shapes the payload with keepFields and renameFields.
Output configuration example
{
// ...
"diffingPlan": {
"pipeline": [ /* ... */ ],
"output": {
"format": "basic",
"idEncoding": "decimal",
"keepFields": [ /* ... */ ],
"renameFields": { /* ... */ }
}
}
}Example 1: The simplest diff
The smallest valid plan runs only the compute-changes source step with no output block. This produces the simplest output a diff job can have: an array of ChangeInstance records containing all the changed data inside each changed EC instance.
What the diffing plan does
compute-changes: Reads the requested changeset range and emits one record per changed EC instance, carrying every changed property captured in the changesets along with the$metachange metadata (e.g.,opandstage).
What the output looks like
- With no
outputconfiguration, records pass through untouched — all raw property values are preserved. The only exception is byte-stream properties (e.g.GeometryStream): they are always abbreviated to their size, such as"{\"bytes\":81}". - Updated elements produce two records: one for the old state (
"stage": "Old") and one for the new state ("stage": "New"). Inserted and deleted elements produce a single record. - Ids use the default hex (
0x...) encoding.
Intended workflow
Use this plan when you want the complete change data and prefer to post-process it yourself. The next examples build on this exact plan, progressively trimming and shaping this raw feed.
Create the job
POST https://api.bentley.com/changedelements/diff HTTP/1.1
Authorization: Bearer <token>
Accept: application/vnd.bentley.itwin-platform.v3+json
Content-Type: application/json
{
"iTwinId": "11111111-1111-1111-1111-111111111111",
"iModelId": "11111111-1111-1111-1111-111111111111",
"startChangeset": 12,
"endChangeset": 18,
"diffingPlan": {
"pipeline": [
{ "name": "compute-changes" }
]
}
}
HTTP/1.1 202 Accepted
{
"job": {
"jobId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"status": "Queued"
}
}Poll for job completion
GET https://api.bentley.com/changedelements/diff/3fa85f64-5717-4562-b3fc-2c963f66afa6?iTwinId=11111111-1111-1111-1111-111111111111&iModelId=11111111-1111-1111-1111-111111111111 HTTP/1.1
Authorization: Bearer <token>
Accept: application/vnd.bentley.itwin-platform.v3+json
HTTP/1.1 200 OK
{
"job": {
"status": "Completed",
"href": "https://example.blob.core.windows.net/example-1.json?sv=..."
}
}Download the result blob
GET https://example.blob.core.windows.net/example-1.json?sv=...
[
{
"ECInstanceId": "0x2",
"ECClassId": "0x100",
"$meta": { "op": "Updated", "stage": "Old" },
"UserLabel": "Wall-A11",
"Category": { "Id": "0x17", "RelECClassId": "0x6c" },
"LastMod": "2026-08-30T12:01:33.000Z"
},
{
"ECInstanceId": "0x2",
"ECClassId": "0x100",
"$meta": { "op": "Updated", "stage": "New" },
"UserLabel": "Wall-A12",
"Category": { "Id": "0x17", "RelECClassId": "0x6c" },
"LastMod": "2026-09-01T09:14:02.000Z"
},
{
"ECInstanceId": "0x3",
"ECClassId": "0x100",
"$meta": { "op": "Inserted", "stage": "New" },
"UserLabel": "Door-B04",
"Category": { "Id": "0x17", "RelECClassId": "0x6c" },
"LastMod": "2026-09-01T10:42:19.000Z"
}
]Example 2: Getting high-level changes
Building on Example 1, this plan keeps the same source step but turns the data into a lightweight record per changed element. It removes the old copy emitted for updates, removes non-elements, and adds readable class names. It utilizes the basic output format to do this. Compare the result blob with Example 1: the same two elements now produce just two minimal records.
What the diffing plan does
compute-changes: Same source step as Example 1.drop-duplicate-updates: Removes the old-state ("stage": "Old") record for each update, keeping only the new one.drop-non-elements: Removes any instance that does not inherit from the BisCore:Element EC base class, effectively keeping only element instances.add-class-full-name: Adds human-readable class full names to the results, making it easier to do ECSQL queries or understand what the elements represent.
What the output format does
"format": "basic": Converts the pipeline records into a simplified structure containing only the essential fields:{ id, classFullName, operation }. If not provided, the full records from Example 1 would be returned instead."idEncoding": "hex": Keeps ids in their usual hex string form:0x.... You could alternatively use"idEncoding": "decimal"to represent ids as decimal numbers.
Intended workflow
Since the results are very simple, containing only elements, their class names, and their operation, this workflow is ideal for quickly identifying what has changed without providing any information about changed properties or element data. This is very similar to what our predefined Basic strategy provides.
Create the job
POST https://api.bentley.com/changedelements/diff HTTP/1.1
Authorization: Bearer <token>
Accept: application/vnd.bentley.itwin-platform.v3+json
Content-Type: application/json
{
"iTwinId": "11111111-1111-1111-1111-111111111111",
"iModelId": "11111111-1111-1111-1111-111111111111",
"startChangeset": 12,
"endChangeset": 18,
"diffingPlan": {
"pipeline": [
{ "name": "compute-changes" },
{ "name": "drop-duplicate-updates" },
{ "name": "drop-non-elements" },
{ "name": "add-class-full-name" }
],
"output": {
"format": "basic",
"idEncoding": "hex"
}
}
}
HTTP/1.1 202 Accepted
{
"job": {
"jobId": "9c799a9b-9cd7-47e1-93ea-64ed5f5d978f",
"status": "Queued"
}
}Poll for job completion
GET https://api.bentley.com/changedelements/diff/9c799a9b-9cd7-47e1-93ea-64ed5f5d978f?iTwinId=11111111-1111-1111-1111-111111111111&iModelId=11111111-1111-1111-1111-111111111111 HTTP/1.1
Authorization: Bearer <token>
Accept: application/vnd.bentley.itwin-platform.v3+json
HTTP/1.1 200 OK
{
"job": {
"status": "Completed",
"href": "https://example.blob.core.windows.net/example-2.json?sv=..."
}
}Download the result blob
GET https://example.blob.core.windows.net/example-2.json?sv=...
[
{
"id": "0x2",
"classFullName": "Generic:PhysicalObject",
"operation": "Updated"
},
{
"id": "0x3",
"classFullName": "Generic:PhysicalObject",
"operation": "Inserted"
}
]Example 3: A fully tailored diff
This final example integrates all the steps from this tutorial into one plan. It builds on Example 2 by enriching each element with its model id, trimming intermediate fields early, and shaping the final payload by selecting and renaming certain fields as suitable for a downstream system.
What the diffing plan does
compute-changes,drop-duplicate-updates,drop-non-elements, andadd-class-full-name: Same source, deduplication, cleanup, and enrichment as Example 2.add-model-id: Enriches each retained element with the id of its containing model.filter-fields.keepOnly: Removes all intermediate properties except those named, while always preservingECInstanceIdby design. Trimming early keeps the processed data small.
What the output configuration does
keepFields: Allow-list applied to the final records; anything not listed is removed.renameFields: Applied last, after field selection, producing the integration-oriented names shown in the result blob (elementId,class,sourceModelId,label). Note that the element id field is always preserved, so renamingECInstanceIdadds the new name alongside it rather than replacing it.
Intended workflow
Use this shape when a downstream consumer needs a small, stable payload with its own naming conventions, without any post-processing on your side.
Create the job
POST https://api.bentley.com/changedelements/diff HTTP/1.1
Authorization: Bearer <token>
Accept: application/vnd.bentley.itwin-platform.v3+json
Content-Type: application/json
{
"iTwinId": "11111111-1111-1111-1111-111111111111",
"iModelId": "11111111-1111-1111-1111-111111111111",
"startChangeset": 12,
"endChangeset": 18,
"diffingPlan": {
"pipeline": [
{ "name": "compute-changes" },
{ "name": "drop-duplicate-updates" },
{ "name": "drop-non-elements" },
{ "name": "add-class-full-name" },
{ "name": "add-model-id" },
{
"name": "filter-fields",
"config": {
"keepOnly": ["ECInstanceId", "classFullName", "modelId", "UserLabel", "$meta"]
}
}
],
"output": {
"keepFields": ["ECInstanceId", "classFullName", "modelId", "UserLabel", "$meta"],
"renameFields": {
"ECInstanceId": "elementId",
"classFullName": "class",
"modelId": "sourceModelId",
"UserLabel": "label"
}
}
}
}
HTTP/1.1 202 Accepted
{
"job": {
"jobId": "b7e3a917-0d9d-49c8-a5e1-f0b6ccf3bc6c",
"status": "Queued"
}
}Poll for job completion
GET https://api.bentley.com/changedelements/diff/b7e3a917-0d9d-49c8-a5e1-f0b6ccf3bc6c?iTwinId=11111111-1111-1111-1111-111111111111&iModelId=11111111-1111-1111-1111-111111111111 HTTP/1.1
Authorization: Bearer <token>
Accept: application/vnd.bentley.itwin-platform.v3+json
HTTP/1.1 200 OK
{
"job": {
"status": "Completed",
"href": "https://example.blob.core.windows.net/example-3.json?sv=..."
}
}Download the result blob
GET https://example.blob.core.windows.net/example-3.json?sv=...
[
{
"ECInstanceId": "0x2",
"elementId": "0x2",
"class": "Generic:PhysicalObject",
"sourceModelId": "0x20",
"label": "Wall-A12",
"$meta": { "op": "Updated", "stage": "New" }
},
{
"ECInstanceId": "0x3",
"elementId": "0x3",
"class": "Generic:PhysicalObject",
"sourceModelId": "0x20",
"label": "Door-B04",
"$meta": { "op": "Inserted", "stage": "New" }
}
]Conclusion
You can now create a custom V3 diffing plan, follow its asynchronous job lifecycle, and download a result shaped for a specific consumer. Start with the smallest pipeline that produces the fields you need, then add filtering, enrichment, or output configuration only when it serves that consumer.
Remember the two validation rules that prevent most configuration mistakes: provide exactly one of strategy or pipeline, and do not set both keepFields and dropFields in one output block. Pipeline step names are case-sensitive, whereas strategy names are case-insensitive.
Was this page helpful?