> ## Documentation Index
> Fetch the complete documentation index at: https://docs.resolve.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Teams API

Create, read, update, and delete ResolveAI teams from your own scripts and pipelines. Each team is one JSON document: its name, description, communication channels, and membership.

> [!NOTE]
>
> **Preview:** the Teams API is enabled per organization. To turn it on, contact `help@resolve.ai`. Until it is on, every endpoint returns `403` with the code `api_disabled`.

## What you can manage

| Part of a team | Teams API |
| :- | :- |
| Name and description | Read and write |
| Membership: directory group bindings and additional members | Read and write |
| Communication channels (Slack, MS Teams) | Read and write |
| Alert filters | Read only |

Resolve also changes channels and alert filters itself, for example when someone accepts a suggestion in Resolve. Expect those changes when you read a team.

## Base URL

```text theme={null}
https://app.resolve.ai/api/v1
```

Use the host you sign in to if it differs from `app.resolve.ai`. All paths below are relative to this URL. Requests and responses use JSON.

## Authentication

Send an API token created by an organization admin in the `Authorization` header:

```http theme={null}
Authorization: Bearer <your-token>
```

Create the token in **Admin** > **API Tokens**. The token acts with its creator's current roles:

* The token decides the organization. A request never names an organization, and a team of another organization returns `404`.
* A missing, invalid, or expired token, or one whose creator is deactivated, returns `401`.
* A token whose creator is not an organization admin returns `403`. If the creator loses the admin role or leaves, create a new token and update your pipeline.

> [!WARNING]
>
> **Security best practice:** Never store credentials in plaintext in configuration files or source control. Keep the token in your CI secret store.

## The team document

```json theme={null}
{
  "id": "Xb2kQ9vLm3TfR7wYc1NpA",
  "version": 7,
  "schemaVersion": 1,
  "spec": {
    "displayName": "Payments",
    "description": "Checkout and ledger services",
    "communicationChannels": {
      "channels": [
        {
          "key": { "integrationInstanceId": "<slack integration id>", "slack": { "channelId": "C07PAYALERTS" } },
          "purposes": ["ALERTS"]
        }
      ]
    },
    "alertFilters": { "filters": [] },
    "membership": {
      "groupBindings": [{ "groupId": "pD7rT2xN9qLs4KvB0cWeJ", "role": "MEMBER" }],
      "additionalMembers": [{ "email": "alice@example.com", "role": "ADMIN" }]
    }
  }
}
```

| Field | Set by | Description |
| :- | :- | :- |
| `id` | Resolve | The team id. It never changes, even when the team is renamed. |
| `version` | Resolve | Changes on every change to the team. Also returned as the `ETag` header. |
| `schemaVersion` | You | The schema version your client knows. Currently `1`. |
| `spec.displayName` | You | The team name, unique among your organization's teams. Required on create. |
| `spec.description` | You | Free text. Optional; empty if left out. |
| `spec.communicationChannels` | You | Slack channels (`channelId`) or MS Teams channels (`conversationId`), each with purposes such as `ALERTS`. Optional. Order has no meaning; responses return channels and purposes sorted. |
| `spec.alertFilters` | Resolve | Read only. A write that contains this section returns `400` with the code `unsupported_section`. |
| `spec.membership` | You | Group bindings and additional members. See [Membership](#membership). |

## Endpoints

| Method and path | Purpose | Required headers | Success |
| :- | :- | :- | :- |
| `GET /teams` | List teams (`id`, `displayName`, `version`), with paging | — | `200` |
| `GET /teams?displayName=<name>` | Find the team with this exact name | — | `200` |
| `POST /teams` | Create a team | — | `201`, `ETag`, `Location` |
| `GET /teams/{id}` | Read a team | — | `200`, `ETag` |
| `PATCH /teams/{id}` | Update the sections you send; add `?dryRun=true` to preview | `If-Match` | `200`, new `ETag` |
| `DELETE /teams/{id}` | Delete a team | `If-Match` | `204` |
| `GET /groups` | Find directory groups to bind to a team | — | `200` |
| `GET /integrations?integrationKey=slack` | Find your connected integrations and their ids | — | `200` |

### List teams

```bash theme={null}
curl -sS -H "Authorization: Bearer $TOKEN" "https://app.resolve.ai/api/v1/teams?limit=50"
```

```json theme={null}
{
  "teams": [
    { "id": "Xb2kQ9vLm3TfR7wYc1NpA", "version": 7, "displayName": "Payments" },
    { "id": "Hn4sD8eJq0ZuK6aGt2VmW", "version": 3, "displayName": "Checkout" }
  ],
  "nextCursor": "Y3VycmVudC1wYWdlLTI"
}
```

`limit` defaults to 100, with a maximum of 500. Pass `nextCursor` back as `?cursor=` for the next page; it is `null` on the last page.

### Create a team

```bash theme={null}
curl -sS -X POST "https://app.resolve.ai/api/v1/teams" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "schemaVersion": 1,
    "spec": {
      "displayName": "Payments",
      "description": "Checkout and ledger services",
      "membership": {
        "groupBindings": [{ "groupId": "pD7rT2xN9qLs4KvB0cWeJ", "role": "MEMBER" }],
        "additionalMembers": [{ "email": "alice@example.com", "role": "ADMIN" }]
      }
    }
  }'
```

The response is `201 Created` with the whole team document, its `ETag`, and `Location: /api/v1/teams/<id>`.

* If a team with that name exists, the response is `409` with the code `name_conflict` and the existing team's id, so a retried create never makes a duplicate.
* Without `membership`, the token's creator joins the new team as an admin. With `membership`, the declared roster is the whole team, so declare the admins you want.

### Update part of a team

Send only the sections you want to change. A section you leave out stays as it is; a section you send replaces that section completely.

```bash theme={null}
curl -sS -X PATCH "https://app.resolve.ai/api/v1/teams/Xb2kQ9vLm3TfR7wYc1NpA" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -H 'If-Match: "7"' \
  -d '{
    "schemaVersion": 1,
    "spec": {
      "membership": {
        "groupBindings": [{ "groupId": "pD7rT2xN9qLs4KvB0cWeJ", "role": "MEMBER" }],
        "additionalMembers": [
          { "email": "alice@example.com", "role": "ADMIN" },
          { "email": "bob@example.com", "role": "MEMBER" }
        ]
      }
    }
  }'
```

The response is the updated document with a new `ETag`.

Add `?dryRun=true` to see the change without saving it. A dry run returns `dryRun: true` and the `changes` per section, and the `ETag` stays the same. A dry run does not run every check, so a taken name or an unknown channel can still preview as a success.

If the team has pending changes in Resolve that are waiting for review and your update touches the same parts, the update succeeds and replaces those pending parts. The response lists them in `supersededProposals`.

### Delete a team

```bash theme={null}
curl -sS -X DELETE "https://app.resolve.ai/api/v1/teams/Xb2kQ9vLm3TfR7wYc1NpA" \
  -H "Authorization: Bearer $TOKEN" \
  -H 'If-Match: "8"'
```

The response is `204 No Content`.

### Find a directory group

```bash theme={null}
curl -sS -H "Authorization: Bearer $TOKEN" \
  "https://app.resolve.ai/api/v1/groups?provider=PAGERDUTY&name=Payments%20on-call"
```

```json theme={null}
{
  "groups": [
    {
      "id": "pD7rT2xN9qLs4KvB0cWeJ",
      "provider": "PAGERDUTY",
      "integrationInstanceId": "<pagerduty integration id>",
      "providerId": "PXYZ123",
      "name": "Payments on-call",
      "memberCount": 6
    }
  ],
  "nextCursor": null
}
```

Filter by `provider` (`SCIM`, `PAGERDUTY`, `IRM`, `ROOTLY`, `DATADOG`, `INCIDENT_IO`), `name`, `providerId`, or `integrationInstanceId`. Use the returned `id` as `groupId`.

## Membership

A team's people come from two sources, and you can use both at once.

* **Group bindings** (`groupBindings`): a directory group and the role its people get. Groups come from SCIM, PagerDuty, Grafana IRM, Rootly, Datadog, or incident.io. Resolve keeps the team in step when people join or leave the group. Bind at most one group per role, all from the same provider.
* **Additional members** (`additionalMembers`): people you add by email. Each email must match a user in your organization; provision users through [SCIM](/directory-sync-scim) first.
* **Roles** are `MEMBER` or `ADMIN`. If a person is in a bound group and is also an additional member, the higher role applies. If they leave the group, the additional-member entry stays.
* **Directory-managed admins:** bind a second group with the role `ADMIN`. That group then becomes the source of admins, and the team's additional admins are removed.
* **Replacing membership:** sending `membership` replaces it. Leaving `membership` out keeps it as it is. Empty lists remove all bindings or additional members.

## Updates and concurrency

* Every read returns the team's version in the `ETag` header. `PATCH` and `DELETE` must send it back unchanged in `If-Match`. Treat the `ETag` as opaque.
* If the team changed since you read it, the write fails with `412` and the code `stale_version`. Read the team again and retry.
* A missing `If-Match` returns `428` with the code `precondition_required`.
* The version changes on every change to the team, from any source: the API, the Resolve UI, Slack, or an accepted suggestion. People joining or leaving a bound group do not change it.

## Errors

Every error has the same body:

```json theme={null}
{
  "error": "Another team already has this displayName",
  "code": "name_conflict",
  "details": { "existingTeamId": "Xb2kQ9vLm3TfR7wYc1NpA" }
}
```

| Status | `code` | When |
| :- | :- | :- |
| `400` | `invalid_input` | The body or a parameter is not valid. `details` lists each field error. |
| `400` | `unknown_email` | An additional member's email matches no user. Nothing is written. `details` lists every unmatched email. |
| `400` | `unsupported_section` | The write contains a read-only section, such as `alertFilters`. |
| `401` | `unauthorized` | The token is missing, invalid, or expired. |
| `403` | `forbidden` | The token's creator is not an organization admin. |
| `403` | `api_disabled` | The Teams API is not enabled for your organization. |
| `404` | `not_found` | No such team or group in your organization. |
| `409` | `name_conflict` | Another team already has this name. `details.existingTeamId` gives its id. |
| `412` | `stale_version` | The team changed since your read. `details.currentVersion` gives the new version. |
| `428` | `precondition_required` | `If-Match` is missing on `PATCH` or `DELETE`. |
| `429` | `rate_limited` | Too many requests for this token. Wait for the seconds in the `Retry-After` header, then retry. |

## Limits

| Item | Limit |
| :- | :- |
| Reads per token | 600 per minute |
| Writes per token | 120 per minute |
| Teams per list page | 100 by default, 500 at most |
| Communication channels per team | 50 |
| Group bindings per team | 2 (one per role) |
| Additional members per team | 500 |

## Compatibility

* Send `schemaVersion` with every write. Resolve changes only the fields your version knows, so an older client keeps working when new fields are added.
* Responses can contain fields your client does not know; ignore them. Requests with unknown fields are refused.
* Treat enum values, such as roles and channel purposes, as open strings. New values can appear.
* Breaking changes ship only under a new path (`/api/v2`), with notice before `/api/v1` changes.

## What's next

* [Team and group mapping](/team-group-mapping): how directory groups map to teams in the Resolve UI.
* [Directory Sync (SCIM)](/directory-sync-scim): provision the users that additional members refer to.
* [Resolve REST API](/resolve-rest-api): query investigations from scripts.
