> For the complete documentation index, see [llms.txt](https://docs.nullify.ai/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.nullify.ai/api-reference/api-reference/api-admin/teams.md).

# Teams and Ownership

A team groups people and the repositories they own, and drives who findings are assigned to. These endpoints manage teams, control which sources create teams automatically, and map findings to teams. Authenticate as described in [Authentication](/api-reference/api-reference/authentication.md); people and roles are covered in [Users](/api-reference/api-reference/api-admin/users.md) and [Identity and Access](/api-reference/api-reference/api-admin/identity-and-access.md).

Every call is scoped to a workspace with a provider query parameter such as `githubOwnerId`, `gitlabGroupId`, `azureOrganizationId`, `bitbucketWorkspaceId` or `installationId`. Which one you pass depends on how your tenant is connected; the call returns 400 when none resolves to a tenant.

Teams have a `source`: `github_teams`, `repository_property`, `atlassian_compass`, `identity_provider` or `manual`. Only manual teams can be created, edited freely and deleted through this API; teams from the other sources are maintained by their sync.

## Teams

### List teams

Returns a page of teams from every source, with lead and member details filled in. Filter with `search` (case-insensitive match on the team name) or `repositoryId` (only teams that own that repository). Page with `limit` (default 200, at most 1,000) and `nextToken`, or jump to a position with `offset`, which wins over `nextToken` when both are sent; see [Pagination](/api-reference/api-reference/pagination.md). A page is also cut short if it would exceed about 4 MiB serialized, so follow `nextToken` until it is empty rather than assuming a full page. `numItems` is the total number of matching teams. Requires `ConfigRead`.

```bash
curl -s \
  -H "Accept: application/json" \
  -H "Authorization: Bearer <TOKEN>" \
  'https://api.<TENANT>.nullify.ai/admin/teams?githubOwnerId=1234&search=platform&limit=50'
```

{% openapi src="/files/1NgvLK12PRq7RFdn97yd" path="/admin/teams" method="get" %}
[nullify-openapi-bundle.yaml](https://3946391995-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FId2OtLCL6O6V3XSRYxtr%2Fuploads%2Fgit-blob-a96d8165240befab9d431c87b16a7387b6914215%2Fnullify-openapi-bundle.yaml?alt=media)
{% endopenapi %}

### Get a team

Returns one team by `teamID`, the `id` of a team from [List teams](#list-teams), with members, leads, repositories and code owner glob patterns filled in. Note the path is `/admin/team/{teamID}` (singular `team`). Returns 404 if the team does not exist in your workspace. Requires `ConfigRead`.

{% openapi src="/files/1NgvLK12PRq7RFdn97yd" path="/admin/team/{teamID}" method="get" %}
[nullify-openapi-bundle.yaml](https://3946391995-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FId2OtLCL6O6V3XSRYxtr%2Fuploads%2Fgit-blob-a96d8165240befab9d431c87b16a7387b6914215%2Fnullify-openapi-bundle.yaml?alt=media)
{% endopenapi %}

### Create a manual team

Creates a team with source `manual`. `name`, `slug`, `privacy`, `messageChannelProvider` and `ticketProjectProvider` are required by the schema and `name` must be non-empty. A new team id is generated on every call, so repeating the call creates another team. `leadId` must be a Nullify user id from [Users](/api-reference/api-reference/api-admin/users.md) (400 otherwise); `memberIds` are not checked on create, so set members with [Update a team](#update-a-team), which validates them. Every `codeOwnerships[].dataSource` must be `manual`. `groundRules` need a non-empty `title` (up to 200 characters) and a `description` of up to 2,000 characters. Internal automation (agents) cannot send `codeOwnerships` and receives 403; a person's token can. Requires `TeamManage`.

Use `"messageChannelProvider":{"id":""}` when the team has no message channel.

```bash
curl -s -X POST \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer <TOKEN>" \
  -d '{"name":"Payments","slug":"payments","privacy":"private","messageChannelProvider":{"id":""},"ticketProjectProvider":{}}' \
  'https://api.<TENANT>.nullify.ai/admin/teams?githubOwnerId=1234'
```

{% openapi src="/files/1NgvLK12PRq7RFdn97yd" path="/admin/teams" method="post" %}
[nullify-openapi-bundle.yaml](https://3946391995-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FId2OtLCL6O6V3XSRYxtr%2Fuploads%2Fgit-blob-a96d8165240befab9d431c87b16a7387b6914215%2Fnullify-openapi-bundle.yaml?alt=media)
{% endopenapi %}

### Update a team

Partially updates a team: only fields you send are changed, and an omitted field keeps its stored value. `groundRules` replaces the stored list, and an empty array clears it. `memberIds` is the complete member list: users you add become manual members that the provider sync keeps, users you leave out are removed and the sync does not add them back, an empty list removes everyone, and omitting `memberIds` leaves membership alone. `codeOwnerships` entries must have `dataSource` `manual`. The `github`, `gitlab` and `jira` objects are merged field by field. `leadId` and added members must be Nullify user ids (400 otherwise); an unknown `teamID` returns 404. Internal automation (agents) cannot send `memberIds` or `codeOwnerships` and receives 403. Changing members or code ownerships queues a background re-sync of finding-to-team mappings. Member and ownership edits are recorded in the audit log. Requires `TeamManage`.

{% openapi src="/files/1NgvLK12PRq7RFdn97yd" path="/admin/teams/{teamID}" method="patch" %}
[nullify-openapi-bundle.yaml](https://3946391995-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FId2OtLCL6O6V3XSRYxtr%2Fuploads%2Fgit-blob-a96d8165240befab9d431c87b16a7387b6914215%2Fnullify-openapi-bundle.yaml?alt=media)
{% endopenapi %}

### Delete a manual team

Soft-deletes a team whose source is `manual`. Teams from any other source return 400, because their sync owns them; to stop a synced source, turn it off with [Update team source preferences](#update-team-source-preferences). Deleting a team that is already deleted succeeds. An unknown `teamID` currently returns 500 rather than 404. Requires `TeamManage`.

{% openapi src="/files/1NgvLK12PRq7RFdn97yd" path="/admin/teams/{teamID}" method="delete" %}
[nullify-openapi-bundle.yaml](https://3946391995-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FId2OtLCL6O6V3XSRYxtr%2Fuploads%2Fgit-blob-a96d8165240befab9d431c87b16a7387b6914215%2Fnullify-openapi-bundle.yaml?alt=media)
{% endopenapi %}

### Merge teams

Combines two or more provider teams into one. Send `sources` (at least two entries, each a `providerId` and `teamId`) and, optionally, `targetTeamId` to merge into an existing team; without it a new manual team is created. Member ids are unioned, code ownerships are appended, ground rules are unioned, and provider ids and message and ticket settings are copied where the target has none. A source that cannot be found is skipped rather than failing the call, and source teams are left in place. The merged team is returned. Requires `TeamManage`.

{% openapi src="/files/1NgvLK12PRq7RFdn97yd" path="/admin/teams/merge" method="post" %}
[nullify-openapi-bundle.yaml](https://3946391995-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FId2OtLCL6O6V3XSRYxtr%2Fuploads%2Fgit-blob-a96d8165240befab9d431c87b16a7387b6914215%2Fnullify-openapi-bundle.yaml?alt=media)
{% endopenapi %}

### Create or update a Compass team

Imports a team from Atlassian Compass. `displayName` is required, and `teamKey` (the Compass team key) and `providerId` are also required even though the schema does not mark them so; missing values return 400. `lead` and `members` are stored as users under the Jira provider using the strings you send as their name and username, and `repositories` accepts `owner/repo`, `owner/repo.git`, GitHub `tree` or `blob` URLs (the sub-path becomes a glob pattern) and `git@host:owner/repo` SSH forms. Repositories that are malformed or not installed in your connected GitHub app are skipped. A team with the same name under Jira is updated in place, otherwise `teamKey` becomes the team id; its Compass code ownerships are replaced with the ones you send. If you send no lead, members or repositories, nothing is created and the call returns 200 with no team. When the call changes the team's code ownerships or members, a background re-sync of finding mappings is started on a best-effort basis; re-sending an unchanged team starts none. Internal automation (agents) cannot send a lead, members or repositories and receives 403. Requires `TeamManage`.

{% openapi src="/files/1NgvLK12PRq7RFdn97yd" path="/admin/teams/compass" method="post" %}
[nullify-openapi-bundle.yaml](https://3946391995-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FId2OtLCL6O6V3XSRYxtr%2Fuploads%2Fgit-blob-a96d8165240befab9d431c87b16a7387b6914215%2Fnullify-openapi-bundle.yaml?alt=media)
{% endopenapi %}

### Make a team the owner of a repository

Makes the team the only manual code owner of an entire repository. Send `repositoryId` in the body; it is a repository id from [Repositories and Code](/api-reference/api-reference/api-admin/repositories-and-code.md). Other teams' manual ownership of that repository is replaced in one transaction, ownership produced by a provider sync or a code owners file is untouched, and the manual ownership survives later syncs. Returns 404 if the team or repository does not exist in your workspace, and 403 for internal automation, which cannot make this change. A background re-sync of finding-to-team mappings is queued and the change is audit logged. Requires `TeamManage`.

{% openapi src="/files/1NgvLK12PRq7RFdn97yd" path="/admin/teams/{teamID}/code-ownership" method="post" %}
[nullify-openapi-bundle.yaml](https://3946391995-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FId2OtLCL6O6V3XSRYxtr%2Fuploads%2Fgit-blob-a96d8165240befab9d431c87b16a7387b6914215%2Fnullify-openapi-bundle.yaml?alt=media)
{% endopenapi %}

### Download the Microsoft Teams app package

Despite its path, this is not part of team ownership: it builds the Microsoft Teams chat app package (manifest and icons) for your Nullify Teams bot, uploads it, and returns a time-limited `presignedUrl` for downloading the zip. Use it when installing the Nullify app in Microsoft Teams. Requires `TeamManage`.

{% openapi src="/files/1NgvLK12PRq7RFdn97yd" path="/admin/teams/appzip" method="get" %}
[nullify-openapi-bundle.yaml](https://3946391995-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FId2OtLCL6O6V3XSRYxtr%2Fuploads%2Fgit-blob-a96d8165240befab9d431c87b16a7387b6914215%2Fnullify-openapi-bundle.yaml?alt=media)
{% endopenapi %}

## Team sources and sync

Teams are created automatically from GitHub Teams and from repository custom properties. These operations switch each source on or off, choose which repository properties define teams, and start or inspect sync runs.

### Get team source preferences

Returns whether the `github_teams` and `repository_property` sources are enabled. If preferences were never saved, both are enabled. Manual teams are always available and have no toggle. Requires `ConfigRead`.

{% openapi src="/files/1NgvLK12PRq7RFdn97yd" path="/admin/team-source-preferences" method="get" %}
[nullify-openapi-bundle.yaml](https://3946391995-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FId2OtLCL6O6V3XSRYxtr%2Fuploads%2Fgit-blob-a96d8165240befab9d431c87b16a7387b6914215%2Fnullify-openapi-bundle.yaml?alt=media)
{% endopenapi %}

### Update team source preferences

Sets `githubTeamsEnabled` and `repositoryPropertyEnabled`; both are required, so send the full state. Turning a source off hides every team from that source, and turning it back on restores them and starts a sync for that source. The response includes `syncQueued`, which is `false` when a sync was needed but could not be started; the daily sync then catches up. Concurrent updates for a tenant are serialized. Requires `TeamManage`.

{% openapi src="/files/1NgvLK12PRq7RFdn97yd" path="/admin/team-source-preferences" method="put" %}
[nullify-openapi-bundle.yaml](https://3946391995-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FId2OtLCL6O6V3XSRYxtr%2Fuploads%2Fgit-blob-a96d8165240befab9d431c87b16a7387b6914215%2Fnullify-openapi-bundle.yaml?alt=media)
{% endopenapi %}

### List repository property team keys

Lists the repository custom property names that this tenant uses to derive teams, one team per distinct property value. Requires `TeamManage`.

{% openapi src="/files/1NgvLK12PRq7RFdn97yd" path="/admin/repository-property-team-keys" method="get" %}
[nullify-openapi-bundle.yaml](https://3946391995-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FId2OtLCL6O6V3XSRYxtr%2Fuploads%2Fgit-blob-a96d8165240befab9d431c87b16a7387b6914215%2Fnullify-openapi-bundle.yaml?alt=media)
{% endopenapi %}

### Create a repository property team key

Adds a property name that defines teams. `providerId` and `propertyKey` are required (the key is trimmed and at most 256 characters). A key already configured for the provider returns 409. A repository-property sync is started in the background; poll [List team sync runs](#list-team-sync-runs) to follow it. Requires `TeamManage`.

{% openapi src="/files/1NgvLK12PRq7RFdn97yd" path="/admin/repository-property-team-keys" method="post" %}
[nullify-openapi-bundle.yaml](https://3946391995-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FId2OtLCL6O6V3XSRYxtr%2Fuploads%2Fgit-blob-a96d8165240befab9d431c87b16a7387b6914215%2Fnullify-openapi-bundle.yaml?alt=media)
{% endopenapi %}

### Delete a repository property team key

Removes a property key, identified by the `keyId` query parameter (the `id` from [List repository property team keys](#list-repository-property-team-keys)). Returns 404 if it does not exist. Teams derived from that property that no longer own anything are cleaned up by the sync run this call starts. Requires `TeamManage`.

{% openapi src="/files/1NgvLK12PRq7RFdn97yd" path="/admin/repository-property-team-keys" method="delete" %}
[nullify-openapi-bundle.yaml](https://3946391995-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FId2OtLCL6O6V3XSRYxtr%2Fuploads%2Fgit-blob-a96d8165240befab9d431c87b16a7387b6914215%2Fnullify-openapi-bundle.yaml?alt=media)
{% endopenapi %}

### List team sync runs

Returns recent team sync runs for your workspace, newest first. Filter with `source` (`github_teams`, `repository_property`, `manual`, `atlassian_compass` or `identity_provider`; anything else returns 400). `limit` defaults to 10 and is capped at 100. Each run has a `status`, `startedAt`, optional `completedAt` and `errorMessage`, and `triggeredBy`. Use it to see whether a sync you started is still running. Requires `TeamManage`.

{% openapi src="/files/1NgvLK12PRq7RFdn97yd" path="/admin/team-sync-runs" method="get" %}
[nullify-openapi-bundle.yaml](https://3946391995-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FId2OtLCL6O6V3XSRYxtr%2Fuploads%2Fgit-blob-a96d8165240befab9d431c87b16a7387b6914215%2Fnullify-openapi-bundle.yaml?alt=media)
{% endopenapi %}

### Trigger a team sync

Asks for a team sync run. Omit `source` to cover both `github_teams` and `repository_property`, or set it to one of those two; other sources return 400 because they cannot be synced on demand. If a run for that source is already pending or running, nothing new starts. The response lists the latest run for each requested source, so a success means a run was requested, not that it finished; poll [List team sync runs](#list-team-sync-runs). Requires `TeamManage`.

```bash
curl -s -X POST \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer <TOKEN>" \
  -d '{"source":"github_teams"}' \
  'https://api.<TENANT>.nullify.ai/admin/team-sync-runs?githubOwnerId=1234'
```

{% openapi src="/files/1NgvLK12PRq7RFdn97yd" path="/admin/team-sync-runs" method="post" %}
[nullify-openapi-bundle.yaml](https://3946391995-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FId2OtLCL6O6V3XSRYxtr%2Fuploads%2Fgit-blob-a96d8165240befab9d431c87b16a7387b6914215%2Fnullify-openapi-bundle.yaml?alt=media)
{% endopenapi %}

## Team findings

Findings are mapped to teams by matching repository ownership, code owners files and team membership, or by a manual assignment. Viewers who are limited to their own teams only see mappings and counts for teams they belong to.

### List findings mapped to every team

Returns each team with its mapped findings. The response is not paginated: it loads up to 1,000 teams and every mapping for each, so it can be large. Prefer [Get findings for a team](#get-findings-for-a-team). Requires `FindingRead`.

{% openapi src="/files/1NgvLK12PRq7RFdn97yd" path="/admin/teams/findings" method="get" %}
[nullify-openapi-bundle.yaml](https://3946391995-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FId2OtLCL6O6V3XSRYxtr%2Fuploads%2Fgit-blob-a96d8165240befab9d431c87b16a7387b6914215%2Fnullify-openapi-bundle.yaml?alt=media)
{% endopenapi %}

### Get findings for a team

Returns one team and every finding mapped to it. For a team a team-scoped viewer does not belong to, you get the team with an empty findings list. An unknown `teamID` currently returns 500 rather than 404. Requires `FindingRead`.

{% openapi src="/files/1NgvLK12PRq7RFdn97yd" path="/admin/teams/{teamID}/findings" method="get" %}
[nullify-openapi-bundle.yaml](https://3946391995-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FId2OtLCL6O6V3XSRYxtr%2Fuploads%2Fgit-blob-a96d8165240befab9d431c87b16a7387b6914215%2Fnullify-openapi-bundle.yaml?alt=media)
{% endopenapi %}

### Assign findings to teams manually

Assigns findings to teams by hand. Send `mappings`, between 1 and 1,000 entries, each with `findingId`, `teamId` and `findingType`. A finding id may appear only once per request, every team must exist in your workspace, and finding types that cannot be assigned to teams are rejected with 400. The response reports `upserted`. Manual assignments are recorded as manual and audit logged. Requires `FindingWrite`.

{% openapi src="/files/1NgvLK12PRq7RFdn97yd" path="/admin/teams/findings" method="post" %}
[nullify-openapi-bundle.yaml](https://3946391995-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FId2OtLCL6O6V3XSRYxtr%2Fuploads%2Fgit-blob-a96d8165240befab9d431c87b16a7387b6914215%2Fnullify-openapi-bundle.yaml?alt=media)
{% endopenapi %}

### Re-run finding-to-team matching

Runs the matcher that maps findings to teams. With no filters it covers the whole workspace; narrow it with `repositoryIds`, `repositoryNames`, `findingIds` or `findingTypes`. By default it runs inline and returns `matched` and `unmatched` counts; a very large tenant can get a partial result with a `message` explaining the cutoff. Set `async` to `true` to hand it to a background worker: the call returns immediately with `async: true` and `matched` and `unmatched` of `-1`, and there is no run id to poll, so check results with [Get findings for a team](#get-findings-for-a-team). Requires `FindingWrite`.

```bash
curl -s -X POST \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer <TOKEN>" \
  -d '{"async":true}' \
  'https://api.<TENANT>.nullify.ai/admin/teams/findings/sync?githubOwnerId=1234'
```

{% openapi src="/files/1NgvLK12PRq7RFdn97yd" path="/admin/teams/findings/sync" method="post" %}
[nullify-openapi-bundle.yaml](https://3946391995-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FId2OtLCL6O6V3XSRYxtr%2Fuploads%2Fgit-blob-a96d8165240befab9d431c87b16a7387b6914215%2Fnullify-openapi-bundle.yaml?alt=media)
{% endopenapi %}

### Get SLA breakdown by team

Given `findingIDs` (note the capitalization), returns for each team the number of those findings that are in SLA (`inSLA`) and out of SLA (`outOfSLA`). Teams with none of the findings, teams deleted since the mapping, and teams a team-scoped viewer cannot read are omitted; an empty list returns an empty result. This is a read-only operation sent as POST because of the body. Requires `FindingRead`.

{% openapi src="/files/1NgvLK12PRq7RFdn97yd" path="/admin/teams/sla-breakdown" method="post" %}
[nullify-openapi-bundle.yaml](https://3946391995-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FId2OtLCL6O6V3XSRYxtr%2Fuploads%2Fgit-blob-a96d8165240befab9d431c87b16a7387b6914215%2Fnullify-openapi-bundle.yaml?alt=media)
{% endopenapi %}

### List events for a team

Returns the unified activity feed for one team. All body fields are optional filters: `services`, `excludeServices`, `findingIds`, `eventTypes`, `excludeEventTypes`, `autofixId`, `prCanonicalId`, `runId` and `campaignId`, plus a time window with `beforeTimeUnix` and `afterTimeUnix`. An unknown service name returns 400. `limit` defaults to 50 and is capped at 200. When `hasMore` is true the response includes `nextCursorTime` and `nextCursorId` for the next request; some internal services and event types are excluded by default. A team-scoped viewer asking for another team gets an empty list. Requires `FindingRead`.

{% openapi src="/files/1NgvLK12PRq7RFdn97yd" path="/admin/teams/{teamID}/events" method="post" %}
[nullify-openapi-bundle.yaml](https://3946391995-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FId2OtLCL6O6V3XSRYxtr%2Fuploads%2Fgit-blob-a96d8165240befab9d431c87b16a7387b6914215%2Fnullify-openapi-bundle.yaml?alt=media)
{% endopenapi %}
