> This page is for version v2 (default).
> For other versions, use one of these documentation indexes:
> - v2 (default): https://autodesk-poc.ferndocs.com/v2/llms.txt
> - v1: https://autodesk-poc.ferndocs.com/v1/llms.txt

> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://autodesk-poc.ferndocs.com/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://autodesk-poc.ferndocs.com/_mcp/server.

# Sync project roles

POST https://developer.api.autodesk.com/construction/admin/v2/projects/{projectId}/roles:sync
Content-Type: application/json

Retrieves project roles for a mobile client, implementing the Mobile Sync 1.1 standard.

A sync begins with an empty `cursorState`. Each response returns a
`pagination.nextPost` containing the exact URL and body for the following request,
so the client replays it until `pagination.moreData` is `false`. Keep the final
`cursorState`: passing it later returns only what changed since.

Use `fields: ["id", "hash"]` to retrieve fingerprints alone, compare them against
what the device already holds, and then re-request only the roles that differ using
`filters.id`.

`current.data` and `deleted.data` are keyed by collection — this endpoint
returns only `roles` — so that a response can carry several collections in
future without a breaking change.

This endpoint supersedes `/construction/admin/v1/projects/{projectId}/roles:sync`,
which remains available and unchanged. Sync status is not duplicated here — continue
to use `/construction/admin/v1/projects/{projectId}/roles:sync-status`, passing the
`cursorState` as its `syncToken`.

Note that the `Authorization` header token can be obtained either via a [three-legged](https://aps.autodesk.com/en/docs/oauth/v2/tutorials/get-3-legged-token/) OAuth flow, or via a [two-legged](https://aps.autodesk.com/en/docs/oauth/v2/tutorials/get-2-legged-token/) Oauth flow *with user impersonation*, for which the `User-Id` header is required.

Reference: https://autodesk-poc.ferndocs.com/acc-admin/project-roles/sync-project-roles

## Authentication

- OAuth2 — send the obtained token as `Authorization: Bearer <token>`
- `Authorization` header (bearer token, required) — **OAuth 2.0 (3-legged)** — user context required. Also accepts a Secure Service Account (SSA) token, which is headless like a two-legged flow but classified as three-legged because it preserves user context. **Authorization Code Flow** Authorization URL: [https://developer.api.autodesk.com/authentication/v2/authorize](https://developer.api.autodesk.com/authentication/v2/authorize) Token URL: [https://developer.api.autodesk.com/authentication/v2/token](https://developer.api.autodesk.com/authentication/v2/token) Refresh URL: [https://developer.api.autodesk.com/authentication/v2/token](https://developer.api.autodesk.com/authentication/v2/token) Scopes: - `data:read` — read your data - `account:read` — read account and project data - `account:write` — modify account and project data

## Request

### Path parameters

- `projectId` (string, required) — The ID of the project. This corresponds to project ID in the [Data Management API](https://aps.autodesk.com/en/docs/data/v2/). To convert a project ID in the Data Management API into a project ID in the Forma API you need to remove the "**b.**\" prefix. For example, a project ID of `b.a4be0c34a-4ab7` translates to a project ID of `a4be0c34a-4ab7`.

### Headers

- `Accept-Language` (string, optional, default: en) — This header is not currently supported in the Hub Admin API.
- `Region` (string, optional) — The region that the data is stored in. For a full list of supported regions, please check [here](https://aps.autodesk.com/en/docs/acc/v1/overview/acc-regions/).
- `User-Id` (string, optional) — Note that this header is not relevant for Hub Admin GET endpoints. The ID of a user on whose behalf your API request is acting. Required if you're using a 2-legged authentication context, which must be *2-legged OAuth2 security with user impersonation*. Your app has access to all users specified by the administrator in the SaaS integrations UI. Provide this header value to identify the user to be affected by the request. You can use either the user's Forma ID (`id`), or their Autodesk ID (`autodeskId`).

### Body (application/json)

This endpoint expects a projectRoleSyncRequest.

- `filters` (ProjectRoleSyncRequestFilters, optional) — Restricts the sync. Omit or send an empty object to sync every role in the project. **Send array values in a consistent order between requests.** The cursor records the filters a sync began with, and the comparison that detects a filter change is order-sensitive for array values, so re-sending the same ids in a different order reads as a new filter: the response carries `overwrite: true` and the client re-downloads the collection from the start. The order of keys within `filters` does not matter, only the order of values inside an array. Replaying `pagination.nextPost.body` verbatim avoids this, since the server echoes the filters back in the order it recorded them.
- `fields` (list of string, optional) — The fields to return on each item. Omit it or send an empty array for the full role. `id` and `hash` are valid values alongside the role's other fields, so `["id", "hash"]` returns fingerprints only. `id` is always present regardless.
- `cursorState` (string, optional) — Opaque pagination and watermark token from the previous response's `pagination.nextPost.body.cursorState`. Empty or absent starts a fresh sync. Changing `filters` while presenting a cursor restarts the sync, and the response carries `pagination.overwrite: true`. Changing `fields` does not. The cursor records the filters only, so a client can fetch `["id", "hash"]`, compare the fingerprints against what it holds, and then re-request the entities that differ under the same cursor without restarting.
- `limit` (integer, optional) — Requested maximum entities per page. Defaults to the server maximum. The floor of 1000 is the Mobile Sync standard's minimum request size. This endpoint serves up to 10000 per page, matching the largest data array the standard's response schema allows, so any value in `1000..10000` is honoured and anything larger is reduced to 10000.

## Response

### 200

The project roles to be synced (added/removed) on the device.

- `current` (ProjectRoleSyncResponseCurrent, required)
- `deleted` (ProjectRoleSyncResponseDeleted, required)
- `pagination` (syncPagination, required) — Pagination and watermark state for the sync.

## Errors

### 400 Bad Request Error

The request could not be understood by the server due to malformed syntax.

- `status` (integer, optional) — The status code for the error.
- `type` (string, optional) — HQ defined error code for the type of error.
- `title` (string, optional) — The title of the error. ex: Bad Request.
- `detail` (string, optional) — A detailed description of the error with possible suggestions for fixing them.
- `id` (string, optional) — A unique id for the request.
- `errors` (list of fieldError, optional) — List of field validation errors.

### 401 Unauthorized Error

Request has not been applied because it lacks valid authentication credentials for the target resource.

- `authenticationUrl` (string, optional) — The url used for authenticating the user.
- `status` (integer, optional) — The status code for the error.
- `type` (string, optional) — HQ defined error code for the type of error.
- `title` (string, optional) — The title of the error. ex: Bad Request.
- `detail` (string, optional) — A detailed description of the error with possible suggestions for fixing them.
- `id` (string, optional) — A unique id for the request.

### 403 Forbidden Error

The server understood the request but refuses to authorize it.

- `status` (integer, optional) — The status code for the error.
- `type` (string, optional) — HQ defined error code for the type of error.
- `title` (string, optional) — The title of the error. ex: Bad Request.
- `detail` (string, optional) — A detailed description of the error with possible suggestions for fixing them.
- `id` (string, optional) — A unique id for the request.

### 404 Not Found Error

The resource cannot be found.

- `status` (integer, optional) — The status code for the error.
- `type` (string, optional) — HQ defined error code for the type of error.
- `title` (string, optional) — The title of the error. ex: Bad Request.
- `detail` (string, optional) — A detailed description of the error with possible suggestions for fixing them.
- `id` (string, optional) — A unique id for the request.

### 406 Not Acceptable Error

The server cannot produce a response matching the list of acceptable values defined in the request.

- `status` (integer, optional) — The status code for the error.
- `type` (string, optional) — HQ defined error code for the type of error.
- `title` (string, optional) — The title of the error. ex: Bad Request.
- `detail` (string, optional) — A detailed description of the error with possible suggestions for fixing them.
- `id` (string, optional) — A unique id for the request.

### 410 Gone Error

Access to the target resource is no longer available.

- `status` (integer, optional) — The status code for the error.
- `type` (string, optional) — HQ defined error code for the type of error.
- `title` (string, optional) — The title of the error. ex: Bad Request.
- `detail` (string, optional) — A detailed description of the error with possible suggestions for fixing them.
- `id` (string, optional) — A unique id for the request.

### 429 Too Many Requests Error

User has sent too many requests in a given amount of time.

- `any`

### 500 Internal Server Error

An unexpected error occurred on the server.

- `status` (integer, optional) — The status code for the error.
- `type` (string, optional) — HQ defined error code for the type of error.
- `title` (string, optional) — The title of the error. ex: Bad Request.
- `detail` (string, optional) — A detailed description of the error with possible suggestions for fixing them.
- `id` (string, optional) — A unique id for the request.

### 503 Service Unavailable Error

Server is not ready to handle the request.

- `status` (integer, optional) — The status code for the error.
- `type` (string, optional) — HQ defined error code for the type of error.
- `title` (string, optional) — The title of the error. ex: Bad Request.
- `detail` (string, optional) — A detailed description of the error with possible suggestions for fixing them.
- `id` (string, optional) — A unique id for the request.

## Types

### ProjectRoleSyncRequestFilters

Restricts the sync. Omit or send an empty object to sync every role in the project. **Send array values in a consistent order between requests.** The cursor records the filters a sync began with, and the comparison that detects a filter change is order-sensitive for array values, so re-sending the same ids in a different order reads as a new filter: the response carries `overwrite: true` and the client re-downloads the collection from the start. The order of keys within `filters` does not matter, only the order of values inside an array. Replaying `pagination.nextPost.body` verbatim avoids this, since the server echoes the filters back in the order it recorded them.

- `id` (list of string, optional) — Restricts the sync to the given role IDs, up to 1000. Applies to `deleted.data` as well as `current.data`, so deletions are never reported for roles outside the set. Unrecognized filter keys are rejected.

### ProjectRoleSyncResponseCurrent

- `data` (ProjectRoleSyncResponseCurrentData, optional) — Keyed by collection so that a single response can carry more than one collection in future. This endpoint returns only `roles`.

### ProjectRoleSyncResponseDeleted

- `data` (ProjectRoleSyncResponseDeletedData, optional) — Keyed by collection, matching `current.data`.

### syncPagination

Pagination and watermark state for the sync.

- `nextPost` (SyncPaginationNextPost, required) — A complete, replayable request for the next page. When `moreData` is false this is where to resume for the next delta.
- `moreData` (boolean, optional) — True when another page is available now. False means the device is up to date; keep the cursor for the next delta.
- `overwrite` (boolean, optional) — Present and true when `filters` changed, which restarts the sync. Changing `fields` alone does not restart it and does not set this flag. The client should discard its local copy of this collection, because entities the previous filter excluded were never sent and cannot be expressed as a delta. Note that re-sending the same filter ids in a different order counts as a change; see `filters`.
- `filteredResults` (integer, optional) — The number of roles matching the filters on this request. Returned in place of `totalResults` whenever `filters` is non-empty, so the two are never reported from the same count.
- `totalResults` (integer, optional) — The total number of roles in the project. Returned on initial sync pages, and only when no `filters` are applied — a filtered request reports `filteredResults` instead.

### fieldError

Error description for a field validation error.

- `type` (string, optional) — HQ defined error code for the type of error.
- `field` (string, optional) — Name of the field associated to the error.
- `title` (string, optional) — Title for the error.
- `detail` (string, optional) — A detailed description of the error so the user knows how to fix it.

### ProjectRoleSyncResponseCurrentData

Keyed by collection so that a single response can carry more than one collection in future. This endpoint returns only `roles`.

- `roles` (list of projectRoleSyncItem, optional) — The project roles created or changed since the previous sync.

### ProjectRoleSyncResponseDeletedData

Keyed by collection, matching `current.data`.

- `roles` (list of string, optional) — The ids of project roles removed since the previous sync.

### SyncPaginationNextPost

A complete, replayable request for the next page. When `moreData` is false this is where to resume for the next delta.

- `url` (string, required) — The URL to POST for the next page.
- `body` (projectRoleSyncRequest, required) — Mobile Sync 1.1 request body. Every property is optional; an empty body starts a full sync of every role.

### projectRoleSyncItem

A project role. Which properties are present depends on the requested `fields`; `id` is always returned.

- `hash` (string, optional) — A checksum identifying this version of the role, as 32 lowercase hex characters (the 128-bit XXH3 variant of xxhash, per the Mobile Sync hashing standard). Equal `id` and `hash` mean the client already holds this version and can skip the item. The digest covers the entity, not the projection, so it is unchanged by `fields` and stays comparable across requests that asked for different fields. It is computed over exactly these properties, whichever ones were requested: `accountId`, `createdAt`, `id`, `isImmutable`, `key`, `memberGroupId`, `name`, `projectId`, `status`, `templateId`, `updatedAt`. `serviceAccesses` is deliberately **excluded**: the hashing standard states that an entity's checksum should not depend on its child collections. A role whose service accesses change therefore keeps the same hash, and a client that detects changes by comparing hashes alone will not observe that change. The property set above is part of the contract rather than an internal detail. Adding or removing a property from it changes every role's hash, so clients that have stored hashes will re-download once.
- `id` (string, optional) — The role's unique ID, and its delta-sync identity. Always returned, whatever `fields` asks for, and the value that appears in `deleted.data` when the role is removed — use it to match an item against locally held data. The Mobile Sync standard names this field `uid`. This API uses `id` because it is the role's own identifier and the name it carries everywhere else here, and because `uid` already means the Autodesk (Oxygen) identifier on this surface.
- `name` (string, optional) — The name of the role. Unlike the other project role endpoints, this one returns predefined role names in English regardless of `Accept-Language`. The name contributes to `hash`, so localizing it would make the same role hash differently on two devices set to different languages, and would re-hash every predefined role whenever a user changed language — forcing a full re-download. `Accept-Language` is still honoured for error messages.
- `key` (string, optional) — The internal key used for translating predefined role names.
- `status` (enum, optional) — The role status.
  - Allowed values: `active`, `inactive`
- `accountId` (string, optional) — The ID of the account the role belongs to.
- `projectId` (string, optional) — The ID of the project the role belongs to.
- `templateId` (string, optional) — The ID of the global role this role was derived from.
- `memberGroupId` (string, optional) — The ID of the Oxygen group backing this role.
- `isImmutable` (boolean, optional) — Indicates whether the role is immutable. If true, the role cannot be modified or deleted.
- `createdAt` (datetime, optional) — The timestamp when the role was created.
- `updatedAt` (datetime, optional) — The timestamp when the role was last updated.
- `serviceAccesses` (list of ProjectRoleSyncItemServiceAccessesItems, optional) — The service access levels granted by this role. Excluded from `hash`.

### projectRoleSyncRequest

Mobile Sync 1.1 request body. Every property is optional; an empty body starts a full sync of every role.

- `filters` (ProjectRoleSyncRequestFilters, optional) — Restricts the sync. Omit or send an empty object to sync every role in the project. **Send array values in a consistent order between requests.** The cursor records the filters a sync began with, and the comparison that detects a filter change is order-sensitive for array values, so re-sending the same ids in a different order reads as a new filter: the response carries `overwrite: true` and the client re-downloads the collection from the start. The order of keys within `filters` does not matter, only the order of values inside an array. Replaying `pagination.nextPost.body` verbatim avoids this, since the server echoes the filters back in the order it recorded them.
- `fields` (list of string, optional) — The fields to return on each item. Omit it or send an empty array for the full role. `id` and `hash` are valid values alongside the role's other fields, so `["id", "hash"]` returns fingerprints only. `id` is always present regardless.
- `cursorState` (string, optional) — Opaque pagination and watermark token from the previous response's `pagination.nextPost.body.cursorState`. Empty or absent starts a fresh sync. Changing `filters` while presenting a cursor restarts the sync, and the response carries `pagination.overwrite: true`. Changing `fields` does not. The cursor records the filters only, so a client can fetch `["id", "hash"]`, compare the fingerprints against what it holds, and then re-request the entities that differ under the same cursor without restarting.
- `limit` (integer, optional) — Requested maximum entities per page. Defaults to the server maximum. The floor of 1000 is the Mobile Sync standard's minimum request size. This endpoint serves up to 10000 per page, matching the largest data array the standard's response schema allows, so any value in `1000..10000` is honoured and anything larger is reduced to 10000.

### ProjectRoleSyncItemServiceAccessesItems

A service, and the access level this role grants to it.

- `serviceName` (string, optional) — A keyword identifying the service, in its internal form.
- `access` (enum, optional) — The default access level this role grants to the service.
  - Allowed values: `project_admin`, `project_user`, `no_access`

## Examples

**Request**

```json
{}
```

**Response**

```json
{
  "current": {
    "data": {
      "roles": [
        {
          "hash": "b5e9c1ad071b3e7fc779cfaa5e523818",
          "id": "287d5cc2-9008-462c-96e5-c9491db85d97",
          "name": "Architect",
          "key": "architect",
          "status": "active",
          "accountId": "a7f0b3d2-4c81-4e59-9b7a-2d6e1f3c8a94",
          "projectId": "3e354e66-ac8b-41dd-9bc1-93fc182c25dd",
          "templateId": "c1d94f27-8b03-4a6e-95f1-7e2b0c4d8a56",
          "memberGroupId": "d53f81ba-6c47-4e29-8fb0-91a2c7e4d605",
          "isImmutable": false,
          "createdAt": "2018-01-01T12:45:00.000Z",
          "updatedAt": "2019-01-01T12:45:00.000Z",
          "serviceAccesses": [
            {
              "serviceName": "doc_manager",
              "access": "project_user"
            }
          ]
        }
      ]
    }
  },
  "deleted": {
    "data": {
      "roles": [
        "287d5cc2-9008-462c-96e5-c9491db85d97"
      ]
    }
  },
  "pagination": {
    "nextPost": {
      "url": "https://developer.api.autodesk.com/construction/admin/v2/projects/3e354e66-ac8b-41dd-9bc1-93fc182c25dd/roles:sync",
      "body": {
        "filters": {
          "id": [
            "287d5cc2-9008-462c-96e5-c9491db85d97"
          ]
        },
        "fields": [
          "hash"
        ],
        "cursorState": "string",
        "limit": 1
      }
    },
    "moreData": false,
    "overwrite": true,
    "filteredResults": 3,
    "totalResults": 42
  }
}
```

**SDK Code**

```python
import requests

url = "https://developer.api.autodesk.com/construction/admin/v2/projects/367d5cc2-9008-462c-96e5-c9491db85d93/roles:sync"

payload = {}
headers = {
    "Accept-Language": "en",
    "Region": "US",
    "User-Id": "6cc15635-2fbd-4f73-afbe-abd833408a1d",
    "Authorization": "<token>.",
    "Content-Type": "application/json"
}

response = requests.post(url, json=payload, headers=headers)

print(response.json())
```

```javascript
const url = 'https://developer.api.autodesk.com/construction/admin/v2/projects/367d5cc2-9008-462c-96e5-c9491db85d93/roles:sync';
const options = {
  method: 'POST',
  headers: {
    'Accept-Language': 'en',
    Region: 'US',
    'User-Id': '6cc15635-2fbd-4f73-afbe-abd833408a1d',
    Authorization: '<token>.',
    'Content-Type': 'application/json'
  },
  body: '{}'
};

try {
  const response = await fetch(url, options);
  const data = await response.json();
  console.log(data);
} catch (error) {
  console.error(error);
}
```

```go
package main

import (
	"fmt"
	"strings"
	"net/http"
	"io"
)

func main() {

	url := "https://developer.api.autodesk.com/construction/admin/v2/projects/367d5cc2-9008-462c-96e5-c9491db85d93/roles:sync"

	payload := strings.NewReader("{}")

	req, _ := http.NewRequest("POST", url, payload)

	req.Header.Add("Accept-Language", "en")
	req.Header.Add("Region", "US")
	req.Header.Add("User-Id", "6cc15635-2fbd-4f73-afbe-abd833408a1d")
	req.Header.Add("Authorization", "<token>.")
	req.Header.Add("Content-Type", "application/json")

	res, _ := http.DefaultClient.Do(req)

	defer res.Body.Close()
	body, _ := io.ReadAll(res.Body)

	fmt.Println(res)
	fmt.Println(string(body))

}
```

```ruby
require 'uri'
require 'net/http'

url = URI("https://developer.api.autodesk.com/construction/admin/v2/projects/367d5cc2-9008-462c-96e5-c9491db85d93/roles:sync")

http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true

request = Net::HTTP::Post.new(url)
request["Accept-Language"] = 'en'
request["Region"] = 'US'
request["User-Id"] = '6cc15635-2fbd-4f73-afbe-abd833408a1d'
request["Authorization"] = '<token>.'
request["Content-Type"] = 'application/json'
request.body = "{}"

response = http.request(request)
puts response.read_body
```

```java
import com.mashape.unirest.http.HttpResponse;
import com.mashape.unirest.http.Unirest;

HttpResponse<String> response = Unirest.post("https://developer.api.autodesk.com/construction/admin/v2/projects/367d5cc2-9008-462c-96e5-c9491db85d93/roles:sync")
  .header("Accept-Language", "en")
  .header("Region", "US")
  .header("User-Id", "6cc15635-2fbd-4f73-afbe-abd833408a1d")
  .header("Authorization", "<token>.")
  .header("Content-Type", "application/json")
  .body("{}")
  .asString();
```

```php
<?php
require_once('vendor/autoload.php');

$client = new \GuzzleHttp\Client();

$response = $client->request('POST', 'https://developer.api.autodesk.com/construction/admin/v2/projects/367d5cc2-9008-462c-96e5-c9491db85d93/roles:sync', [
  'body' => '{}',
  'headers' => [
    'Accept-Language' => 'en',
    'Authorization' => '<token>.',
    'Content-Type' => 'application/json',
    'Region' => 'US',
    'User-Id' => '6cc15635-2fbd-4f73-afbe-abd833408a1d',
  ],
]);

echo $response->getBody();
```

```csharp
using RestSharp;

var client = new RestClient("https://developer.api.autodesk.com/construction/admin/v2/projects/367d5cc2-9008-462c-96e5-c9491db85d93/roles:sync");
var request = new RestRequest(Method.POST);
request.AddHeader("Accept-Language", "en");
request.AddHeader("Region", "US");
request.AddHeader("User-Id", "6cc15635-2fbd-4f73-afbe-abd833408a1d");
request.AddHeader("Authorization", "<token>.");
request.AddHeader("Content-Type", "application/json");
request.AddParameter("application/json", "{}", ParameterType.RequestBody);
IRestResponse response = client.Execute(request);
```

```swift
import Foundation

let headers = [
  "Accept-Language": "en",
  "Region": "US",
  "User-Id": "6cc15635-2fbd-4f73-afbe-abd833408a1d",
  "Authorization": "<token>.",
  "Content-Type": "application/json"
]
let parameters = [] as [String : Any]

let postData = JSONSerialization.data(withJSONObject: parameters, options: [])

let request = NSMutableURLRequest(url: NSURL(string: "https://developer.api.autodesk.com/construction/admin/v2/projects/367d5cc2-9008-462c-96e5-c9491db85d93/roles:sync")! as URL,
                                        cachePolicy: .useProtocolCachePolicy,
                                    timeoutInterval: 10.0)
request.httpMethod = "POST"
request.allHTTPHeaderFields = headers
request.httpBody = postData as Data

let session = URLSession.shared
let dataTask = session.dataTask(with: request as URLRequest, completionHandler: { (data, response, error) -> Void in
  if (error != nil) {
    print(error as Any)
  } else {
    let httpResponse = response as? HTTPURLResponse
    print(httpResponse)
  }
})

dataTask.resume()
```