# Teams

Groups of members. A team can be an escalation target, and its members inherit the team's permissions.

A team's members come from two sources: people added by hand, and people
added by your directory. [Replace a team's members](#setTeamMembers) changes
only the first. A team managed by your directory cannot be renamed or archived
here. See [Team administration](/docs/web-app/team-administration) for the
app's membership, permissions and archiving features.

Permissions granted to a team are inherited by every member, and follow the roster
as it changes.

## List teams

`GET /api/v1/teams`

Auth: Reviewer session cookie (`withhuman_session`) or personal API key (`Authorization: Bearer $WITHHUMAN_API_KEY`)

Requires: `team.read`

Returns the teams you can read. With an organization-wide grant, that is every team. With a grant scoped to a team, only that team.

### Request

| Parameter | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `status` | query | string | no | Select live teams (the default), archived teams, or all teams. |
| `limit` | query | integer | no | How many teams to return. Defaults to 200. |

### Response

| Status | Body | Description |
| --- | --- | --- |
| 200 | object | The teams |
| 400 | ErrorResponse | The status filter is invalid |
| 401 | ErrorResponse | You are not signed in |
| 403 | ErrorResponse | You cannot read teams |

Response body (200):

- `teams` · array<Team> · required
  - `id` · uuid · required
  - `name` · string · required
  - `directory_managed` · boolean · required: Whether your directory owns this team. Such a team cannot be renamed or archived here.
  - `member_count` · integer · required: How many members the team has, from every source.
  - `assignments` · array<RoleAssignment> · required: The roles the team holds. Every member inherits them.
    - `id` · uuid · required
    - `organization_id` · uuid · required
    - `principal_kind` · enum · required: What holds the role. One of `membership`, `team`, `agent`.
    - `principal_id` · uuid · required: The membership, team or agent that holds it.
    - `scope_kind` · enum · required: Where the role applies. One of `organization`, `team`, `agent`.
    - `scope_id` · string: The team id or the agent slug. Absent for `organization`.
    - `scope_name` · string: The team's name, for a team scope.
    - `source` · enum · required: Whether a person granted it or your directory did. One of `manual`, `directory`.
    - `source_key` · string: The directory group that produced it. Present for `directory` only.
    - `granted_by_actor_id` · uuid: The member who granted it. Present for `manual` only.
    - `via_team_id` · uuid: The team the role is inherited through. Present when a member holds it through a team rather than directly.
    - `via_team_name` · string: That team's name.
    - `created_at` · date-time · required
    - `permissions` · array<string> · required
    - `is_owner` · boolean · required
    - `permission_policy_id` · uuid
  - `escalation_policy` · TeamEscalationPolicyStatus · required: The team's escalation policy lineage. Both revisions are null when the team has never had a policy; active_revision alone is null when every revision is archived. Without an active revision a path level that targets the team notifies every member at once.
    - `active_revision` · int64 · required
    - `latest_revision` · int64 · required
  - `archived_at` · date-time · required: When the team was archived. Null for a live team.
  - `created_at` · date-time · required

### Example

```bash
curl -X GET "$WITHHUMAN_URL/api/v1/teams" \
  -H "Authorization: Bearer $WITHHUMAN_API_KEY"
```

200 response

```json
{
  "teams": [
    {
      "id": "7ab8c8ec-7b2d-4fd6-9b52-752f9515eb71",
      "name": "Payments approvers",
      "directory_managed": true,
      "member_count": 1,
      "assignments": [
        {
          "id": "7ab8c8ec-7b2d-4fd6-9b52-752f9515eb71",
          "organization_id": "7ab8c8ec-7b2d-4fd6-9b52-752f9515eb71",
          "principal_kind": "membership",
          "principal_id": "7ab8c8ec-7b2d-4fd6-9b52-752f9515eb71",
          "scope_kind": "organization",
          "scope_id": "string",
          "scope_name": "string",
          "source": "manual",
          "source_key": "string",
          "granted_by_actor_id": "7ab8c8ec-7b2d-4fd6-9b52-752f9515eb71",
          "via_team_id": "7ab8c8ec-7b2d-4fd6-9b52-752f9515eb71",
          "via_team_name": "string",
          "created_at": "2026-09-08T12:02:11Z",
          "permissions": [
            "string"
          ],
          "is_owner": true,
          "permission_policy_id": "7ab8c8ec-7b2d-4fd6-9b52-752f9515eb71"
        }
      ],
      "escalation_policy": {
        "active_revision": 1,
        "latest_revision": 1
      },
      "archived_at": "2026-09-08T12:02:11Z",
      "created_at": "2026-09-08T12:02:11Z"
    }
  ]
}
```

## Create a team

`POST /api/v1/teams`

Auth: Reviewer session cookie (`withhuman_session`) or personal API key (`Authorization: Bearer $WITHHUMAN_API_KEY`)

Requires: `team.write`

Creates an empty team. Add members and roles afterwards. Names are unique among live teams in the organization.

### Request

Request body:

- `name` · string · required: The team's name. Unique among live teams in the organization.

### Response

| Status | Body | Description |
| --- | --- | --- |
| 201 | object | The new team |
| 400 | ErrorResponse | The name is empty |
| 403 | ErrorResponse | You cannot create teams |
| 409 | ErrorResponse | A team with this name already exists |

Response body (201):

- `team` · Team · required: A group of members. A team can be an escalation target, and its members inherit the roles it holds. A team may also own an escalation policy that runs when a path level names it.
  - `id` · uuid · required
  - `name` · string · required
  - `directory_managed` · boolean · required: Whether your directory owns this team. Such a team cannot be renamed or archived here.
  - `member_count` · integer · required: How many members the team has, from every source.
  - `assignments` · array<RoleAssignment> · required: The roles the team holds. Every member inherits them.
    - `id` · uuid · required
    - `organization_id` · uuid · required
    - `principal_kind` · enum · required: What holds the role. One of `membership`, `team`, `agent`.
    - `principal_id` · uuid · required: The membership, team or agent that holds it.
    - `scope_kind` · enum · required: Where the role applies. One of `organization`, `team`, `agent`.
    - `scope_id` · string: The team id or the agent slug. Absent for `organization`.
    - `scope_name` · string: The team's name, for a team scope.
    - `source` · enum · required: Whether a person granted it or your directory did. One of `manual`, `directory`.
    - `source_key` · string: The directory group that produced it. Present for `directory` only.
    - `granted_by_actor_id` · uuid: The member who granted it. Present for `manual` only.
    - `via_team_id` · uuid: The team the role is inherited through. Present when a member holds it through a team rather than directly.
    - `via_team_name` · string: That team's name.
    - `created_at` · date-time · required
    - `permissions` · array<string> · required
    - `is_owner` · boolean · required
    - `permission_policy_id` · uuid
  - `escalation_policy` · TeamEscalationPolicyStatus · required: The team's escalation policy lineage. Both revisions are null when the team has never had a policy; active_revision alone is null when every revision is archived. Without an active revision a path level that targets the team notifies every member at once.
    - `active_revision` · int64 · required
    - `latest_revision` · int64 · required
  - `archived_at` · date-time · required: When the team was archived. Null for a live team.
  - `created_at` · date-time · required

### Example

```bash
curl -X POST "$WITHHUMAN_URL/api/v1/teams" \
  -H "Authorization: Bearer $WITHHUMAN_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "name": "Payments approvers"
}'
```

201 response

```json
{
  "team": {
    "id": "7ab8c8ec-7b2d-4fd6-9b52-752f9515eb71",
    "name": "Payments approvers",
    "directory_managed": true,
    "member_count": 1,
    "assignments": [
      {
        "id": "7ab8c8ec-7b2d-4fd6-9b52-752f9515eb71",
        "organization_id": "7ab8c8ec-7b2d-4fd6-9b52-752f9515eb71",
        "principal_kind": "membership",
        "principal_id": "7ab8c8ec-7b2d-4fd6-9b52-752f9515eb71",
        "scope_kind": "organization",
        "scope_id": "string",
        "scope_name": "string",
        "source": "manual",
        "source_key": "string",
        "granted_by_actor_id": "7ab8c8ec-7b2d-4fd6-9b52-752f9515eb71",
        "via_team_id": "7ab8c8ec-7b2d-4fd6-9b52-752f9515eb71",
        "via_team_name": "string",
        "created_at": "2026-09-08T12:02:11Z",
        "permissions": [
          "string"
        ],
        "is_owner": true,
        "permission_policy_id": "7ab8c8ec-7b2d-4fd6-9b52-752f9515eb71"
      }
    ],
    "escalation_policy": {
      "active_revision": 1,
      "latest_revision": 1
    },
    "archived_at": "2026-09-08T12:02:11Z",
    "created_at": "2026-09-08T12:02:11Z"
  }
}
```

## Retrieve a team

`GET /api/v1/teams/{id}`

Auth: Reviewer session cookie (`withhuman_session`) or personal API key (`Authorization: Bearer $WITHHUMAN_API_KEY`)

Requires: `team.read`

Returns one team with its members. The list includes members added by hand and members added by your directory, and says which for each.

### Request

| Parameter | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `id` | path | uuid | yes | The team's id. |

### Response

| Status | Body | Description |
| --- | --- | --- |
| 200 | object | The team and its members |
| 401 | ErrorResponse | You are not signed in |
| 404 | ErrorResponse | No such team |

Response body (200):

- `team` · Team · required: A group of members. A team can be an escalation target, and its members inherit the roles it holds. A team may also own an escalation policy that runs when a path level names it.
  - `id` · uuid · required
  - `name` · string · required
  - `directory_managed` · boolean · required: Whether your directory owns this team. Such a team cannot be renamed or archived here.
  - `member_count` · integer · required: How many members the team has, from every source.
  - `assignments` · array<RoleAssignment> · required: The roles the team holds. Every member inherits them.
    - `id` · uuid · required
    - `organization_id` · uuid · required
    - `principal_kind` · enum · required: What holds the role. One of `membership`, `team`, `agent`.
    - `principal_id` · uuid · required: The membership, team or agent that holds it.
    - `scope_kind` · enum · required: Where the role applies. One of `organization`, `team`, `agent`.
    - `scope_id` · string: The team id or the agent slug. Absent for `organization`.
    - `scope_name` · string: The team's name, for a team scope.
    - `source` · enum · required: Whether a person granted it or your directory did. One of `manual`, `directory`.
    - `source_key` · string: The directory group that produced it. Present for `directory` only.
    - `granted_by_actor_id` · uuid: The member who granted it. Present for `manual` only.
    - `via_team_id` · uuid: The team the role is inherited through. Present when a member holds it through a team rather than directly.
    - `via_team_name` · string: That team's name.
    - `created_at` · date-time · required
    - `permissions` · array<string> · required
    - `is_owner` · boolean · required
    - `permission_policy_id` · uuid
  - `escalation_policy` · TeamEscalationPolicyStatus · required: The team's escalation policy lineage. Both revisions are null when the team has never had a policy; active_revision alone is null when every revision is archived. Without an active revision a path level that targets the team notifies every member at once.
    - `active_revision` · int64 · required
    - `latest_revision` · int64 · required
  - `archived_at` · date-time · required: When the team was archived. Null for a live team.
  - `created_at` · date-time · required
- `members` · array<TeamMember> · required
  - `membership_id` · uuid · required
  - `display_name` · string · required
  - `email` · string · required
  - `status` · enum · required: The member's status in the organization. One of `active`, `suspended`, `deprovisioned`.
  - `sources` · array<enum> · required: How the person got on the team: added by hand, by your directory, or both.

### Example

```bash
curl -X GET "$WITHHUMAN_URL/api/v1/teams/{id}" \
  -H "Authorization: Bearer $WITHHUMAN_API_KEY"
```

200 response

```json
{
  "team": {
    "id": "7ab8c8ec-7b2d-4fd6-9b52-752f9515eb71",
    "name": "Payments approvers",
    "directory_managed": true,
    "member_count": 1,
    "assignments": [
      {
        "id": "7ab8c8ec-7b2d-4fd6-9b52-752f9515eb71",
        "organization_id": "7ab8c8ec-7b2d-4fd6-9b52-752f9515eb71",
        "principal_kind": "membership",
        "principal_id": "7ab8c8ec-7b2d-4fd6-9b52-752f9515eb71",
        "scope_kind": "organization",
        "scope_id": "string",
        "scope_name": "string",
        "source": "manual",
        "source_key": "string",
        "granted_by_actor_id": "7ab8c8ec-7b2d-4fd6-9b52-752f9515eb71",
        "via_team_id": "7ab8c8ec-7b2d-4fd6-9b52-752f9515eb71",
        "via_team_name": "string",
        "created_at": "2026-09-08T12:02:11Z",
        "permissions": [
          "string"
        ],
        "is_owner": true,
        "permission_policy_id": "7ab8c8ec-7b2d-4fd6-9b52-752f9515eb71"
      }
    ],
    "escalation_policy": {
      "active_revision": 1,
      "latest_revision": 1
    },
    "archived_at": "2026-09-08T12:02:11Z",
    "created_at": "2026-09-08T12:02:11Z"
  },
  "members": [
    {
      "membership_id": "7ab8c8ec-7b2d-4fd6-9b52-752f9515eb71",
      "display_name": "Ada Lovelace",
      "email": "ada@example.com",
      "status": "active",
      "sources": [
        "manual"
      ]
    }
  ]
}
```

## Rename a team

`PATCH /api/v1/teams/{id}`

Auth: Reviewer session cookie (`withhuman_session`) or personal API key (`Authorization: Bearer $WITHHUMAN_API_KEY`)

Requires: `team.write`

Changes a team's name. Teams managed by your directory cannot be renamed here, because the directory owns their names.

### Request

| Parameter | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `id` | path | uuid | yes | The team's id. |

Request body:

- `name` · string · required: The new name. Unique among live teams in the organization.

### Response

| Status | Body | Description |
| --- | --- | --- |
| 200 | object | The renamed team |
| 400 | ErrorResponse | The name is empty |
| 403 | ErrorResponse | You cannot change teams |
| 404 | ErrorResponse | No such team |
| 409 | ErrorResponse | The team is archived or managed by your directory, or a live team with this name already exists |

Response body (200):

- `team` · Team · required: A group of members. A team can be an escalation target, and its members inherit the roles it holds. A team may also own an escalation policy that runs when a path level names it.
  - `id` · uuid · required
  - `name` · string · required
  - `directory_managed` · boolean · required: Whether your directory owns this team. Such a team cannot be renamed or archived here.
  - `member_count` · integer · required: How many members the team has, from every source.
  - `assignments` · array<RoleAssignment> · required: The roles the team holds. Every member inherits them.
    - `id` · uuid · required
    - `organization_id` · uuid · required
    - `principal_kind` · enum · required: What holds the role. One of `membership`, `team`, `agent`.
    - `principal_id` · uuid · required: The membership, team or agent that holds it.
    - `scope_kind` · enum · required: Where the role applies. One of `organization`, `team`, `agent`.
    - `scope_id` · string: The team id or the agent slug. Absent for `organization`.
    - `scope_name` · string: The team's name, for a team scope.
    - `source` · enum · required: Whether a person granted it or your directory did. One of `manual`, `directory`.
    - `source_key` · string: The directory group that produced it. Present for `directory` only.
    - `granted_by_actor_id` · uuid: The member who granted it. Present for `manual` only.
    - `via_team_id` · uuid: The team the role is inherited through. Present when a member holds it through a team rather than directly.
    - `via_team_name` · string: That team's name.
    - `created_at` · date-time · required
    - `permissions` · array<string> · required
    - `is_owner` · boolean · required
    - `permission_policy_id` · uuid
  - `escalation_policy` · TeamEscalationPolicyStatus · required: The team's escalation policy lineage. Both revisions are null when the team has never had a policy; active_revision alone is null when every revision is archived. Without an active revision a path level that targets the team notifies every member at once.
    - `active_revision` · int64 · required
    - `latest_revision` · int64 · required
  - `archived_at` · date-time · required: When the team was archived. Null for a live team.
  - `created_at` · date-time · required

### Example

```bash
curl -X PATCH "$WITHHUMAN_URL/api/v1/teams/{id}" \
  -H "Authorization: Bearer $WITHHUMAN_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "name": "Payments approvers"
}'
```

200 response

```json
{
  "team": {
    "id": "7ab8c8ec-7b2d-4fd6-9b52-752f9515eb71",
    "name": "Payments approvers",
    "directory_managed": true,
    "member_count": 1,
    "assignments": [
      {
        "id": "7ab8c8ec-7b2d-4fd6-9b52-752f9515eb71",
        "organization_id": "7ab8c8ec-7b2d-4fd6-9b52-752f9515eb71",
        "principal_kind": "membership",
        "principal_id": "7ab8c8ec-7b2d-4fd6-9b52-752f9515eb71",
        "scope_kind": "organization",
        "scope_id": "string",
        "scope_name": "string",
        "source": "manual",
        "source_key": "string",
        "granted_by_actor_id": "7ab8c8ec-7b2d-4fd6-9b52-752f9515eb71",
        "via_team_id": "7ab8c8ec-7b2d-4fd6-9b52-752f9515eb71",
        "via_team_name": "string",
        "created_at": "2026-09-08T12:02:11Z",
        "permissions": [
          "string"
        ],
        "is_owner": true,
        "permission_policy_id": "7ab8c8ec-7b2d-4fd6-9b52-752f9515eb71"
      }
    ],
    "escalation_policy": {
      "active_revision": 1,
      "latest_revision": 1
    },
    "archived_at": "2026-09-08T12:02:11Z",
    "created_at": "2026-09-08T12:02:11Z"
  }
}
```

## Archive a team

`DELETE /api/v1/teams/{id}`

Auth: Reviewer session cookie (`withhuman_session`) or personal API key (`Authorization: Bearer $WITHHUMAN_API_KEY`)

Requires: `team.write`

Archives a team permanently. Its identity, roster, and policy history stay readable. Its members lose inherited roles and the active policy is deactivated. Archived teams cannot be edited or targeted by routing. Repeating this operation succeeds without changing the archive date.

Archiving is refused while the team is managed by your directory, is targeted by an active escalation path, is targeted by the path revision of a request still waiting for review.

### Request

| Parameter | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `id` | path | uuid | yes | The team's id. |

### Response

| Status | Body | Description |
| --- | --- | --- |
| 204 |  | The team is archived |
| 403 | ErrorResponse | You cannot archive teams |
| 404 | ErrorResponse | No such team |
| 409 | ErrorResponse | The team is managed by your directory, is targeted by an escalation path or a pending request |

### Example

```bash
curl -X DELETE "$WITHHUMAN_URL/api/v1/teams/{id}" \
  -H "Authorization: Bearer $WITHHUMAN_API_KEY"
```

## Replace a team's members

`PUT /api/v1/teams/{id}/members`

Auth: Reviewer session cookie (`withhuman_session`) or personal API key (`Authorization: Bearer $WITHHUMAN_API_KEY`)

Requires: `team.member.write`

Replaces the members added by hand with the given set. Send an empty list to remove them all. Members added by your directory are not touched. A team's members are the union of both.

Removing a member that the team's active escalation policy still names is refused with `team_member_in_escalation_policy`; `details` carries `team_id`, `revision`, and the `membership_ids` being removed. Pass `force` to remove them anyway. The level naming them then skips them until the policy is edited.

### Request

| Parameter | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `id` | path | uuid | yes | The team's id. |

Request body:

- `membership_ids` · array<uuid> · required: The membership ids of the people to add by hand.
- `force` · boolean: Remove members the active escalation policy still names.

### Response

| Status | Body | Description |
| --- | --- | --- |
| 200 | object | The team's members after the change |
| 400 | ErrorResponse | A membership id does not belong to this organization |
| 403 | ErrorResponse | You cannot change team members |
| 404 | ErrorResponse | No such team |
| 409 | ErrorResponse | The team is archived, or its active escalation policy still names a member being removed: `error.code` is `team_member_in_escalation_policy` |

Response body (200):

- `members` · array<TeamMember> · required
  - `membership_id` · uuid · required
  - `display_name` · string · required
  - `email` · string · required
  - `status` · enum · required: The member's status in the organization. One of `active`, `suspended`, `deprovisioned`.
  - `sources` · array<enum> · required: How the person got on the team: added by hand, by your directory, or both.

### Example

```bash
curl -X PUT "$WITHHUMAN_URL/api/v1/teams/{id}/members" \
  -H "Authorization: Bearer $WITHHUMAN_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "membership_ids": [
    "7ab8c8ec-7b2d-4fd6-9b52-752f9515eb71"
  ],
  "force": true
}'
```

200 response

```json
{
  "members": [
    {
      "membership_id": "7ab8c8ec-7b2d-4fd6-9b52-752f9515eb71",
      "display_name": "Ada Lovelace",
      "email": "ada@example.com",
      "status": "active",
      "sources": [
        "manual"
      ]
    }
  ]
}
```

## Read the team's active escalation policy revision

`GET /api/v1/teams/{id}/escalation-policy`

Auth: Reviewer session cookie (`withhuman_session`) or personal API key (`Authorization: Bearer $WITHHUMAN_API_KEY`)

Requires: `team.read`

The revision a path level that targets the team runs. Warnings name user targets who are no longer active members and members who cannot decide. 404 while no revision is active, in which case a level that targets the team notifies every member at once.

### Request

| Parameter | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `id` | path | uuid | yes | The team's id. |

### Response

| Status | Body | Description |
| --- | --- | --- |
| 200 | TeamEscalationPolicyRevision | Active immutable revision with its full document |
| 401 | ErrorResponse | Error response. A 403 from a permission check carries ForbiddenDetails in error.details. |
| 403 | ErrorResponse | Error response. A 403 from a permission check carries ForbiddenDetails in error.details. |
| 404 | ErrorResponse | Error response. A 403 from a permission check carries ForbiddenDetails in error.details. |

Response body (200):

- `id` · uuid · required
- `team_id` · uuid · required
- `revision` · int64 · required
- `is_active` · boolean · required
- `created_by_actor_id` · uuid · required
- `created_at` · date-time · required
- `working_hours` · array<WorkingHoursSet> · required
  - `id` · string · required: A key for the set, unique within the document.
  - `name` · string · required: A display name.
  - `timezone` · string · required: An IANA timezone name, such as `Europe/London`.
  - `intervals` · array<WeekdayInterval> · required: The open windows. At least one.
    - `weekdays` · array<enum> · required: The days the window applies to.
    - `start` · string · required: When the window opens, as `HH:MM` in 24-hour time.
    - `end` · string · required: When the window closes, as `HH:MM` in 24-hour time.
- `nodes` · array<EscalationPathNode> · required
  - `id` · uuid · required: An id for the node, unique within the path.
  - `type` · enum · required: The kind of node. One of `level`, `if_else`, `defer`.
  - `targets` · array<EscalationTarget>: `level` only. The people and teams to notify.
    - `type` · enum · required One of `user`, `team`, `broadcast`.
    - `id` · uuid: Required for user and team, absent for broadcast.
  - `escalate_after` · string: `level` only. How long to wait for a decision before moving to the next node. A duration from `1m` to `168h`.
  - `raise_urgency` · boolean: `level` only. Raise the request to `interrupt` urgency before notifying.
  - `conditions` · array<EscalationPathCondition>: `if_else` only. Every condition must hold for `then` to run. From 1 to 10.
    - `type` · enum · required: The kind of test. `working_hours` tests whether a named window is open. `urgency` tests the request's current urgency. `request` tests the request itself. One of `working_hours`, `urgency`, `request`.
    - `working_hours_id` · string: `working_hours` only. The set to test.
    - `active` · boolean: `working_hours` only. `true` tests that the window is open, `false` that it is closed.
    - `urgency` · enum: `urgency` only. The urgency the request must have. One of `standard`, `interrupt`.
    - `when` · CELLeafCondition | CELAllCondition | CELAnyCondition | CELSomeCondition | CELEveryCondition: A condition tree. A leaf tests one field. all and any combine conditions. some and every test the items of a list field.
  - `then` · array<EscalationPathNode>: `if_else` only. The nodes to run when every condition holds. An empty list moves on to the next node.
  - `else` · array<EscalationPathNode>: `if_else` only. The nodes to run otherwise. An empty list moves on to the next node.
  - `working_hours_id` · string: `defer` only. The working-hours set to wait for.
  - `standard_only` · boolean: `defer` only. Let `interrupt` requests skip the wait.
- `repeat` · EscalationPathRepeat: Run the path again from the top when the nodes run out without a decision. Every condition is evaluated again on each run.
  - `times` · integer · required: How many extra runs, from 1 to 9.
  - `after` · string · required: How long to wait before each extra run. A duration from `1m` to `168h`.
- `document_version` · integer · required
- `warnings` · array<TeamEscalationPolicyWarning> · required: Advisory findings about the document against the team's roster and grants. Never a refusal: the policy narrows who is asked within the team, it never grants. Creation and activation report members who cannot decide; reads of the active revision report that and every target who left the team; reads of a historical revision report departed targets only.
  - `code` · enum · required: team_members_cannot_decide means count of the team's member_count members hold no request.decide grant. target_not_member means the user target in membership_id is no longer an active member of the team, so the level naming them skips them. One of `team_members_cannot_decide`, `target_not_member`.
  - `membership_id` · uuid: The departed target. Present for target_not_member only.
  - `display_name` · string: The departed target's name, while the roster still lists them under another status.
  - `count` · integer · required: Members of the team who cannot decide requests; 0 for target_not_member.
  - `member_count` · integer · required: Active members of the team; 0 for target_not_member.

### Example

```bash
curl -X GET "$WITHHUMAN_URL/api/v1/teams/{id}/escalation-policy" \
  -H "Authorization: Bearer $WITHHUMAN_API_KEY"
```

200 response

```json
{
  "id": "7ab8c8ec-7b2d-4fd6-9b52-752f9515eb71",
  "team_id": "7ab8c8ec-7b2d-4fd6-9b52-752f9515eb71",
  "revision": 1,
  "is_active": true,
  "created_by_actor_id": "7ab8c8ec-7b2d-4fd6-9b52-752f9515eb71",
  "created_at": "2026-09-08T12:02:11Z",
  "working_hours": [
    {
      "id": "uk-office",
      "name": "UK office hours",
      "timezone": "Europe/London",
      "intervals": [
        {
          "weekdays": [
            "mon"
          ],
          "start": "09:00",
          "end": "18:00"
        }
      ]
    }
  ],
  "nodes": [
    {
      "id": "7ab8c8ec-7b2d-4fd6-9b52-752f9515eb71",
      "type": "level",
      "targets": [
        {
          "type": "user",
          "id": "7ab8c8ec-7b2d-4fd6-9b52-752f9515eb71"
        }
      ],
      "escalate_after": "15m",
      "raise_urgency": true,
      "conditions": [
        {
          "type": "working_hours",
          "working_hours_id": "string",
          "active": true,
          "urgency": "standard",
          "when": {
            "id": "7ab8c8ec-7b2d-4fd6-9b52-752f9515eb71",
            "field": "/request/arguments/amount_cents",
            "operator": "exists",
            "value": {}
          }
        }
      ],
      "then": [
        {}
      ],
      "else": [
        {}
      ],
      "working_hours_id": "string",
      "standard_only": true
    }
  ],
  "repeat": {
    "times": 1,
    "after": "1h"
  },
  "document_version": 1,
  "warnings": [
    {
      "code": "team_members_cannot_decide",
      "membership_id": "7ab8c8ec-7b2d-4fd6-9b52-752f9515eb71",
      "display_name": "string",
      "count": 1,
      "member_count": 1
    }
  ]
}
```

## List immutable team escalation policy revisions

`GET /api/v1/teams/{id}/escalation-policy/revisions`

Auth: Reviewer session cookie (`withhuman_session`) or personal API key (`Authorization: Bearer $WITHHUMAN_API_KEY`)

Requires: `team.read`

### Request

| Parameter | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `id` | path | uuid | yes | The team's id. |
| `limit` | query | integer | no | How many revisions to return. Defaults to 50. |

### Response

| Status | Body | Description |
| --- | --- | --- |
| 200 | object | Revision history ordered newest first |
| 401 | ErrorResponse | Error response. A 403 from a permission check carries ForbiddenDetails in error.details. |
| 403 | ErrorResponse | Error response. A 403 from a permission check carries ForbiddenDetails in error.details. |

Response body (200):

- `revisions` · array<TeamEscalationPolicyRevisionSummary> · required
  - `id` · uuid · required
  - `team_id` · uuid · required
  - `revision` · int64 · required
  - `is_active` · boolean · required
  - `created_by_actor_id` · uuid · required
  - `created_at` · date-time · required

### Example

```bash
curl -X GET "$WITHHUMAN_URL/api/v1/teams/{id}/escalation-policy/revisions" \
  -H "Authorization: Bearer $WITHHUMAN_API_KEY"
```

200 response

```json
{
  "revisions": [
    {
      "id": "7ab8c8ec-7b2d-4fd6-9b52-752f9515eb71",
      "team_id": "7ab8c8ec-7b2d-4fd6-9b52-752f9515eb71",
      "revision": 1,
      "is_active": true,
      "created_by_actor_id": "7ab8c8ec-7b2d-4fd6-9b52-752f9515eb71",
      "created_at": "2026-09-08T12:02:11Z"
    }
  ]
}
```

## Create an inactive immutable team escalation policy revision

`POST /api/v1/teams/{id}/escalation-policy/revisions`

Auth: Reviewer session cookie (`withhuman_session`) or personal API key (`Authorization: Bearer $WITHHUMAN_API_KEY`)

Requires: `team.escalation.write`

Validates the complete document before creating the next inactive revision. Levels name members of the team by membership id or broadcast to the whole team; a user target who is not an active member of the team is refused, and team targets belong to organization escalation paths.

### Request

| Parameter | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `id` | path | uuid | yes | The team's id. |

Request body:

- `working_hours` · array<WorkingHoursSet> · required
  - `id` · string · required: A key for the set, unique within the document.
  - `name` · string · required: A display name.
  - `timezone` · string · required: An IANA timezone name, such as `Europe/London`.
  - `intervals` · array<WeekdayInterval> · required: The open windows. At least one.
    - `weekdays` · array<enum> · required: The days the window applies to.
    - `start` · string · required: When the window opens, as `HH:MM` in 24-hour time.
    - `end` · string · required: When the window closes, as `HH:MM` in 24-hour time.
- `nodes` · array<EscalationPathNode> · required
  - `id` · uuid · required: An id for the node, unique within the path.
  - `type` · enum · required: The kind of node. One of `level`, `if_else`, `defer`.
  - `targets` · array<EscalationTarget>: `level` only. The people and teams to notify.
    - `type` · enum · required One of `user`, `team`, `broadcast`.
    - `id` · uuid: Required for user and team, absent for broadcast.
  - `escalate_after` · string: `level` only. How long to wait for a decision before moving to the next node. A duration from `1m` to `168h`.
  - `raise_urgency` · boolean: `level` only. Raise the request to `interrupt` urgency before notifying.
  - `conditions` · array<EscalationPathCondition>: `if_else` only. Every condition must hold for `then` to run. From 1 to 10.
    - `type` · enum · required: The kind of test. `working_hours` tests whether a named window is open. `urgency` tests the request's current urgency. `request` tests the request itself. One of `working_hours`, `urgency`, `request`.
    - `working_hours_id` · string: `working_hours` only. The set to test.
    - `active` · boolean: `working_hours` only. `true` tests that the window is open, `false` that it is closed.
    - `urgency` · enum: `urgency` only. The urgency the request must have. One of `standard`, `interrupt`.
    - `when` · CELLeafCondition | CELAllCondition | CELAnyCondition | CELSomeCondition | CELEveryCondition: A condition tree. A leaf tests one field. all and any combine conditions. some and every test the items of a list field.
  - `then` · array<EscalationPathNode>: `if_else` only. The nodes to run when every condition holds. An empty list moves on to the next node.
  - `else` · array<EscalationPathNode>: `if_else` only. The nodes to run otherwise. An empty list moves on to the next node.
  - `working_hours_id` · string: `defer` only. The working-hours set to wait for.
  - `standard_only` · boolean: `defer` only. Let `interrupt` requests skip the wait.
- `repeat` · EscalationPathRepeat: Run the path again from the top when the nodes run out without a decision. Every condition is evaluated again on each run.
  - `times` · integer · required: How many extra runs, from 1 to 9.
  - `after` · string · required: How long to wait before each extra run. A duration from `1m` to `168h`.

### Response

| Status | Body | Description |
| --- | --- | --- |
| 201 | TeamEscalationPolicyRevision | Created inactive immutable revision |
| 400 | ErrorResponse | Error response. A 403 from a permission check carries ForbiddenDetails in error.details. |
| 401 | ErrorResponse | Error response. A 403 from a permission check carries ForbiddenDetails in error.details. |
| 403 | ErrorResponse | Error response. A 403 from a permission check carries ForbiddenDetails in error.details. |
| 404 | ErrorResponse | Error response. A 403 from a permission check carries ForbiddenDetails in error.details. |
| 409 | ErrorResponse | The team is archived |

Response body (201):

- `id` · uuid · required
- `team_id` · uuid · required
- `revision` · int64 · required
- `is_active` · boolean · required
- `created_by_actor_id` · uuid · required
- `created_at` · date-time · required
- `working_hours` · array<WorkingHoursSet> · required
  - `id` · string · required: A key for the set, unique within the document.
  - `name` · string · required: A display name.
  - `timezone` · string · required: An IANA timezone name, such as `Europe/London`.
  - `intervals` · array<WeekdayInterval> · required: The open windows. At least one.
    - `weekdays` · array<enum> · required: The days the window applies to.
    - `start` · string · required: When the window opens, as `HH:MM` in 24-hour time.
    - `end` · string · required: When the window closes, as `HH:MM` in 24-hour time.
- `nodes` · array<EscalationPathNode> · required
  - `id` · uuid · required: An id for the node, unique within the path.
  - `type` · enum · required: The kind of node. One of `level`, `if_else`, `defer`.
  - `targets` · array<EscalationTarget>: `level` only. The people and teams to notify.
    - `type` · enum · required One of `user`, `team`, `broadcast`.
    - `id` · uuid: Required for user and team, absent for broadcast.
  - `escalate_after` · string: `level` only. How long to wait for a decision before moving to the next node. A duration from `1m` to `168h`.
  - `raise_urgency` · boolean: `level` only. Raise the request to `interrupt` urgency before notifying.
  - `conditions` · array<EscalationPathCondition>: `if_else` only. Every condition must hold for `then` to run. From 1 to 10.
    - `type` · enum · required: The kind of test. `working_hours` tests whether a named window is open. `urgency` tests the request's current urgency. `request` tests the request itself. One of `working_hours`, `urgency`, `request`.
    - `working_hours_id` · string: `working_hours` only. The set to test.
    - `active` · boolean: `working_hours` only. `true` tests that the window is open, `false` that it is closed.
    - `urgency` · enum: `urgency` only. The urgency the request must have. One of `standard`, `interrupt`.
    - `when` · CELLeafCondition | CELAllCondition | CELAnyCondition | CELSomeCondition | CELEveryCondition: A condition tree. A leaf tests one field. all and any combine conditions. some and every test the items of a list field.
  - `then` · array<EscalationPathNode>: `if_else` only. The nodes to run when every condition holds. An empty list moves on to the next node.
  - `else` · array<EscalationPathNode>: `if_else` only. The nodes to run otherwise. An empty list moves on to the next node.
  - `working_hours_id` · string: `defer` only. The working-hours set to wait for.
  - `standard_only` · boolean: `defer` only. Let `interrupt` requests skip the wait.
- `repeat` · EscalationPathRepeat: Run the path again from the top when the nodes run out without a decision. Every condition is evaluated again on each run.
  - `times` · integer · required: How many extra runs, from 1 to 9.
  - `after` · string · required: How long to wait before each extra run. A duration from `1m` to `168h`.
- `document_version` · integer · required
- `warnings` · array<TeamEscalationPolicyWarning> · required: Advisory findings about the document against the team's roster and grants. Never a refusal: the policy narrows who is asked within the team, it never grants. Creation and activation report members who cannot decide; reads of the active revision report that and every target who left the team; reads of a historical revision report departed targets only.
  - `code` · enum · required: team_members_cannot_decide means count of the team's member_count members hold no request.decide grant. target_not_member means the user target in membership_id is no longer an active member of the team, so the level naming them skips them. One of `team_members_cannot_decide`, `target_not_member`.
  - `membership_id` · uuid: The departed target. Present for target_not_member only.
  - `display_name` · string: The departed target's name, while the roster still lists them under another status.
  - `count` · integer · required: Members of the team who cannot decide requests; 0 for target_not_member.
  - `member_count` · integer · required: Active members of the team; 0 for target_not_member.

### Example

```bash
curl -X POST "$WITHHUMAN_URL/api/v1/teams/{id}/escalation-policy/revisions" \
  -H "Authorization: Bearer $WITHHUMAN_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "working_hours": [
    {
      "id": "uk-office",
      "name": "UK office hours",
      "timezone": "Europe/London",
      "intervals": [
        {
          "weekdays": [
            "mon"
          ],
          "start": "09:00",
          "end": "18:00"
        }
      ]
    }
  ],
  "nodes": [
    {
      "id": "7ab8c8ec-7b2d-4fd6-9b52-752f9515eb71",
      "type": "level",
      "targets": [
        {
          "type": "user",
          "id": "7ab8c8ec-7b2d-4fd6-9b52-752f9515eb71"
        }
      ],
      "escalate_after": "15m",
      "raise_urgency": true,
      "conditions": [
        {
          "type": "working_hours",
          "working_hours_id": "string",
          "active": true,
          "urgency": "standard",
          "when": {
            "id": "7ab8c8ec-7b2d-4fd6-9b52-752f9515eb71",
            "field": "/request/arguments/amount_cents",
            "operator": "exists",
            "value": {}
          }
        }
      ],
      "then": [
        {}
      ],
      "else": [
        {}
      ],
      "working_hours_id": "string",
      "standard_only": true
    }
  ],
  "repeat": {
    "times": 1,
    "after": "1h"
  }
}'
```

201 response

```json
{
  "id": "7ab8c8ec-7b2d-4fd6-9b52-752f9515eb71",
  "team_id": "7ab8c8ec-7b2d-4fd6-9b52-752f9515eb71",
  "revision": 1,
  "is_active": true,
  "created_by_actor_id": "7ab8c8ec-7b2d-4fd6-9b52-752f9515eb71",
  "created_at": "2026-09-08T12:02:11Z",
  "working_hours": [
    {
      "id": "uk-office",
      "name": "UK office hours",
      "timezone": "Europe/London",
      "intervals": [
        {
          "weekdays": [
            "mon"
          ],
          "start": "09:00",
          "end": "18:00"
        }
      ]
    }
  ],
  "nodes": [
    {
      "id": "7ab8c8ec-7b2d-4fd6-9b52-752f9515eb71",
      "type": "level",
      "targets": [
        {
          "type": "user",
          "id": "7ab8c8ec-7b2d-4fd6-9b52-752f9515eb71"
        }
      ],
      "escalate_after": "15m",
      "raise_urgency": true,
      "conditions": [
        {
          "type": "working_hours",
          "working_hours_id": "string",
          "active": true,
          "urgency": "standard",
          "when": {
            "id": "7ab8c8ec-7b2d-4fd6-9b52-752f9515eb71",
            "field": "/request/arguments/amount_cents",
            "operator": "exists",
            "value": {}
          }
        }
      ],
      "then": [
        {}
      ],
      "else": [
        {}
      ],
      "working_hours_id": "string",
      "standard_only": true
    }
  ],
  "repeat": {
    "times": 1,
    "after": "1h"
  },
  "document_version": 1,
  "warnings": [
    {
      "code": "team_members_cannot_decide",
      "membership_id": "7ab8c8ec-7b2d-4fd6-9b52-752f9515eb71",
      "display_name": "string",
      "count": 1,
      "member_count": 1
    }
  ]
}
```

## Read one immutable team escalation policy revision

`GET /api/v1/teams/{id}/escalation-policy/revisions/{revision}`

Auth: Reviewer session cookie (`withhuman_session`) or personal API key (`Authorization: Bearer $WITHHUMAN_API_KEY`)

Requires: `team.read`

Warnings name user targets who are no longer active members, so a rollback candidate can be judged before it is activated.

### Request

| Parameter | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `id` | path | uuid | yes | The team's id. |
| `revision` | path | int64 | yes | The revision number. Revisions count up from 1. |

### Response

| Status | Body | Description |
| --- | --- | --- |
| 200 | TeamEscalationPolicyRevision | Historical revision with its full document |
| 400 | ErrorResponse | Error response. A 403 from a permission check carries ForbiddenDetails in error.details. |
| 401 | ErrorResponse | Error response. A 403 from a permission check carries ForbiddenDetails in error.details. |
| 403 | ErrorResponse | Error response. A 403 from a permission check carries ForbiddenDetails in error.details. |
| 404 | ErrorResponse | Error response. A 403 from a permission check carries ForbiddenDetails in error.details. |

Response body (200):

- `id` · uuid · required
- `team_id` · uuid · required
- `revision` · int64 · required
- `is_active` · boolean · required
- `created_by_actor_id` · uuid · required
- `created_at` · date-time · required
- `working_hours` · array<WorkingHoursSet> · required
  - `id` · string · required: A key for the set, unique within the document.
  - `name` · string · required: A display name.
  - `timezone` · string · required: An IANA timezone name, such as `Europe/London`.
  - `intervals` · array<WeekdayInterval> · required: The open windows. At least one.
    - `weekdays` · array<enum> · required: The days the window applies to.
    - `start` · string · required: When the window opens, as `HH:MM` in 24-hour time.
    - `end` · string · required: When the window closes, as `HH:MM` in 24-hour time.
- `nodes` · array<EscalationPathNode> · required
  - `id` · uuid · required: An id for the node, unique within the path.
  - `type` · enum · required: The kind of node. One of `level`, `if_else`, `defer`.
  - `targets` · array<EscalationTarget>: `level` only. The people and teams to notify.
    - `type` · enum · required One of `user`, `team`, `broadcast`.
    - `id` · uuid: Required for user and team, absent for broadcast.
  - `escalate_after` · string: `level` only. How long to wait for a decision before moving to the next node. A duration from `1m` to `168h`.
  - `raise_urgency` · boolean: `level` only. Raise the request to `interrupt` urgency before notifying.
  - `conditions` · array<EscalationPathCondition>: `if_else` only. Every condition must hold for `then` to run. From 1 to 10.
    - `type` · enum · required: The kind of test. `working_hours` tests whether a named window is open. `urgency` tests the request's current urgency. `request` tests the request itself. One of `working_hours`, `urgency`, `request`.
    - `working_hours_id` · string: `working_hours` only. The set to test.
    - `active` · boolean: `working_hours` only. `true` tests that the window is open, `false` that it is closed.
    - `urgency` · enum: `urgency` only. The urgency the request must have. One of `standard`, `interrupt`.
    - `when` · CELLeafCondition | CELAllCondition | CELAnyCondition | CELSomeCondition | CELEveryCondition: A condition tree. A leaf tests one field. all and any combine conditions. some and every test the items of a list field.
  - `then` · array<EscalationPathNode>: `if_else` only. The nodes to run when every condition holds. An empty list moves on to the next node.
  - `else` · array<EscalationPathNode>: `if_else` only. The nodes to run otherwise. An empty list moves on to the next node.
  - `working_hours_id` · string: `defer` only. The working-hours set to wait for.
  - `standard_only` · boolean: `defer` only. Let `interrupt` requests skip the wait.
- `repeat` · EscalationPathRepeat: Run the path again from the top when the nodes run out without a decision. Every condition is evaluated again on each run.
  - `times` · integer · required: How many extra runs, from 1 to 9.
  - `after` · string · required: How long to wait before each extra run. A duration from `1m` to `168h`.
- `document_version` · integer · required
- `warnings` · array<TeamEscalationPolicyWarning> · required: Advisory findings about the document against the team's roster and grants. Never a refusal: the policy narrows who is asked within the team, it never grants. Creation and activation report members who cannot decide; reads of the active revision report that and every target who left the team; reads of a historical revision report departed targets only.
  - `code` · enum · required: team_members_cannot_decide means count of the team's member_count members hold no request.decide grant. target_not_member means the user target in membership_id is no longer an active member of the team, so the level naming them skips them. One of `team_members_cannot_decide`, `target_not_member`.
  - `membership_id` · uuid: The departed target. Present for target_not_member only.
  - `display_name` · string: The departed target's name, while the roster still lists them under another status.
  - `count` · integer · required: Members of the team who cannot decide requests; 0 for target_not_member.
  - `member_count` · integer · required: Active members of the team; 0 for target_not_member.

### Example

```bash
curl -X GET "$WITHHUMAN_URL/api/v1/teams/{id}/escalation-policy/revisions/{revision}" \
  -H "Authorization: Bearer $WITHHUMAN_API_KEY"
```

200 response

```json
{
  "id": "7ab8c8ec-7b2d-4fd6-9b52-752f9515eb71",
  "team_id": "7ab8c8ec-7b2d-4fd6-9b52-752f9515eb71",
  "revision": 1,
  "is_active": true,
  "created_by_actor_id": "7ab8c8ec-7b2d-4fd6-9b52-752f9515eb71",
  "created_at": "2026-09-08T12:02:11Z",
  "working_hours": [
    {
      "id": "uk-office",
      "name": "UK office hours",
      "timezone": "Europe/London",
      "intervals": [
        {
          "weekdays": [
            "mon"
          ],
          "start": "09:00",
          "end": "18:00"
        }
      ]
    }
  ],
  "nodes": [
    {
      "id": "7ab8c8ec-7b2d-4fd6-9b52-752f9515eb71",
      "type": "level",
      "targets": [
        {
          "type": "user",
          "id": "7ab8c8ec-7b2d-4fd6-9b52-752f9515eb71"
        }
      ],
      "escalate_after": "15m",
      "raise_urgency": true,
      "conditions": [
        {
          "type": "working_hours",
          "working_hours_id": "string",
          "active": true,
          "urgency": "standard",
          "when": {
            "id": "7ab8c8ec-7b2d-4fd6-9b52-752f9515eb71",
            "field": "/request/arguments/amount_cents",
            "operator": "exists",
            "value": {}
          }
        }
      ],
      "then": [
        {}
      ],
      "else": [
        {}
      ],
      "working_hours_id": "string",
      "standard_only": true
    }
  ],
  "repeat": {
    "times": 1,
    "after": "1h"
  },
  "document_version": 1,
  "warnings": [
    {
      "code": "team_members_cannot_decide",
      "membership_id": "7ab8c8ec-7b2d-4fd6-9b52-752f9515eb71",
      "display_name": "string",
      "count": 1,
      "member_count": 1
    }
  ]
}
```

## Activate or roll back to a team escalation policy revision

`POST /api/v1/teams/{id}/escalation-policy/revisions/{revision}/activate`

Auth: Reviewer session cookie (`withhuman_session`) or personal API key (`Authorization: Bearer $WITHHUMAN_API_KEY`)

Requires: `team.escalation.activate`

Atomically switches the team's active revision. If-Match must be "0" when no revision is active. The stored document is checked against the roster again, so a revision naming someone who has since left the team is refused. Requests already routed keep the revision they pinned.

### Request

| Parameter | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `id` | path | uuid | yes | The team's id. |
| `revision` | path | int64 | yes | The revision number. Revisions count up from 1. |
| `If-Match` | header | string | yes | The revision you expect to be active, quoted, for example `"2"`. Take it from the `ETag` of your last read. If the active revision changed in the meantime, the call fails with 412 and nothing changes. Escalation paths and webhook endpoints accept `"0"` when no revision is active. Pipelines always have an active revision. |

### Response

| Status | Body | Description |
| --- | --- | --- |
| 200 | TeamEscalationPolicyRevision | Activated revision |
| 400 | ErrorResponse | Error response. A 403 from a permission check carries ForbiddenDetails in error.details. |
| 401 | ErrorResponse | Error response. A 403 from a permission check carries ForbiddenDetails in error.details. |
| 403 | ErrorResponse | Error response. A 403 from a permission check carries ForbiddenDetails in error.details. |
| 404 | ErrorResponse | Error response. A 403 from a permission check carries ForbiddenDetails in error.details. |
| 409 | ErrorResponse | The team is archived |
| 412 | ErrorResponse | If-Match no longer equals the active revision |
| 428 | ErrorResponse | The required If-Match header was omitted |

Response body (200):

- `id` · uuid · required
- `team_id` · uuid · required
- `revision` · int64 · required
- `is_active` · boolean · required
- `created_by_actor_id` · uuid · required
- `created_at` · date-time · required
- `working_hours` · array<WorkingHoursSet> · required
  - `id` · string · required: A key for the set, unique within the document.
  - `name` · string · required: A display name.
  - `timezone` · string · required: An IANA timezone name, such as `Europe/London`.
  - `intervals` · array<WeekdayInterval> · required: The open windows. At least one.
    - `weekdays` · array<enum> · required: The days the window applies to.
    - `start` · string · required: When the window opens, as `HH:MM` in 24-hour time.
    - `end` · string · required: When the window closes, as `HH:MM` in 24-hour time.
- `nodes` · array<EscalationPathNode> · required
  - `id` · uuid · required: An id for the node, unique within the path.
  - `type` · enum · required: The kind of node. One of `level`, `if_else`, `defer`.
  - `targets` · array<EscalationTarget>: `level` only. The people and teams to notify.
    - `type` · enum · required One of `user`, `team`, `broadcast`.
    - `id` · uuid: Required for user and team, absent for broadcast.
  - `escalate_after` · string: `level` only. How long to wait for a decision before moving to the next node. A duration from `1m` to `168h`.
  - `raise_urgency` · boolean: `level` only. Raise the request to `interrupt` urgency before notifying.
  - `conditions` · array<EscalationPathCondition>: `if_else` only. Every condition must hold for `then` to run. From 1 to 10.
    - `type` · enum · required: The kind of test. `working_hours` tests whether a named window is open. `urgency` tests the request's current urgency. `request` tests the request itself. One of `working_hours`, `urgency`, `request`.
    - `working_hours_id` · string: `working_hours` only. The set to test.
    - `active` · boolean: `working_hours` only. `true` tests that the window is open, `false` that it is closed.
    - `urgency` · enum: `urgency` only. The urgency the request must have. One of `standard`, `interrupt`.
    - `when` · CELLeafCondition | CELAllCondition | CELAnyCondition | CELSomeCondition | CELEveryCondition: A condition tree. A leaf tests one field. all and any combine conditions. some and every test the items of a list field.
  - `then` · array<EscalationPathNode>: `if_else` only. The nodes to run when every condition holds. An empty list moves on to the next node.
  - `else` · array<EscalationPathNode>: `if_else` only. The nodes to run otherwise. An empty list moves on to the next node.
  - `working_hours_id` · string: `defer` only. The working-hours set to wait for.
  - `standard_only` · boolean: `defer` only. Let `interrupt` requests skip the wait.
- `repeat` · EscalationPathRepeat: Run the path again from the top when the nodes run out without a decision. Every condition is evaluated again on each run.
  - `times` · integer · required: How many extra runs, from 1 to 9.
  - `after` · string · required: How long to wait before each extra run. A duration from `1m` to `168h`.
- `document_version` · integer · required
- `warnings` · array<TeamEscalationPolicyWarning> · required: Advisory findings about the document against the team's roster and grants. Never a refusal: the policy narrows who is asked within the team, it never grants. Creation and activation report members who cannot decide; reads of the active revision report that and every target who left the team; reads of a historical revision report departed targets only.
  - `code` · enum · required: team_members_cannot_decide means count of the team's member_count members hold no request.decide grant. target_not_member means the user target in membership_id is no longer an active member of the team, so the level naming them skips them. One of `team_members_cannot_decide`, `target_not_member`.
  - `membership_id` · uuid: The departed target. Present for target_not_member only.
  - `display_name` · string: The departed target's name, while the roster still lists them under another status.
  - `count` · integer · required: Members of the team who cannot decide requests; 0 for target_not_member.
  - `member_count` · integer · required: Active members of the team; 0 for target_not_member.

### Example

```bash
curl -X POST "$WITHHUMAN_URL/api/v1/teams/{id}/escalation-policy/revisions/{revision}/activate" \
  -H "Authorization: Bearer $WITHHUMAN_API_KEY" \
  -H "If-Match: "2""
```

200 response

```json
{
  "id": "7ab8c8ec-7b2d-4fd6-9b52-752f9515eb71",
  "team_id": "7ab8c8ec-7b2d-4fd6-9b52-752f9515eb71",
  "revision": 1,
  "is_active": true,
  "created_by_actor_id": "7ab8c8ec-7b2d-4fd6-9b52-752f9515eb71",
  "created_at": "2026-09-08T12:02:11Z",
  "working_hours": [
    {
      "id": "uk-office",
      "name": "UK office hours",
      "timezone": "Europe/London",
      "intervals": [
        {
          "weekdays": [
            "mon"
          ],
          "start": "09:00",
          "end": "18:00"
        }
      ]
    }
  ],
  "nodes": [
    {
      "id": "7ab8c8ec-7b2d-4fd6-9b52-752f9515eb71",
      "type": "level",
      "targets": [
        {
          "type": "user",
          "id": "7ab8c8ec-7b2d-4fd6-9b52-752f9515eb71"
        }
      ],
      "escalate_after": "15m",
      "raise_urgency": true,
      "conditions": [
        {
          "type": "working_hours",
          "working_hours_id": "string",
          "active": true,
          "urgency": "standard",
          "when": {
            "id": "7ab8c8ec-7b2d-4fd6-9b52-752f9515eb71",
            "field": "/request/arguments/amount_cents",
            "operator": "exists",
            "value": {}
          }
        }
      ],
      "then": [
        {}
      ],
      "else": [
        {}
      ],
      "working_hours_id": "string",
      "standard_only": true
    }
  ],
  "repeat": {
    "times": 1,
    "after": "1h"
  },
  "document_version": 1,
  "warnings": [
    {
      "code": "team_members_cannot_decide",
      "membership_id": "7ab8c8ec-7b2d-4fd6-9b52-752f9515eb71",
      "display_name": "string",
      "count": 1,
      "member_count": 1
    }
  ]
}
```

## Archive the team's escalation policy by deactivating its current revision

`DELETE /api/v1/teams/{id}/escalation-policy/active`

Auth: Reviewer session cookie (`withhuman_session`) or personal API key (`Authorization: Bearer $WITHHUMAN_API_KEY`)

Requires: `team.escalation.activate`

Refused when the team is archived. Otherwise, a path level that targets the team falls back to notifying every member at once; requests already routed keep their pinned revision.

### Request

| Parameter | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `id` | path | uuid | yes | The team's id. |
| `If-Match` | header | string | yes | The revision you expect to be active, quoted, for example `"2"`. Take it from the `ETag` of your last read. If the active revision changed in the meantime, the call fails with 412 and nothing changes. Escalation paths and webhook endpoints accept `"0"` when no revision is active. Pipelines always have an active revision. |

### Response

| Status | Body | Description |
| --- | --- | --- |
| 204 |  | Current revision deactivated |
| 400 | ErrorResponse | Error response. A 403 from a permission check carries ForbiddenDetails in error.details. |
| 401 | ErrorResponse | Error response. A 403 from a permission check carries ForbiddenDetails in error.details. |
| 403 | ErrorResponse | Error response. A 403 from a permission check carries ForbiddenDetails in error.details. |
| 404 | ErrorResponse | Error response. A 403 from a permission check carries ForbiddenDetails in error.details. |
| 409 | ErrorResponse | The team is archived |
| 412 | ErrorResponse | If-Match no longer equals the active revision |
| 428 | ErrorResponse | The required If-Match header was omitted |

### Example

```bash
curl -X DELETE "$WITHHUMAN_URL/api/v1/teams/{id}/escalation-policy/active" \
  -H "Authorization: Bearer $WITHHUMAN_API_KEY" \
  -H "If-Match: "2""
```

## Get teams permission policies

`GET /api/v1/teams/{id}/permission_policies`

Auth: Reviewer session cookie (`withhuman_session`) or personal API key (`Authorization: Bearer $WITHHUMAN_API_KEY`)

Self-service: acts on the caller's own account, no permission needed.

### Request

| Parameter | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `id` | path | uuid | yes |  |

### Response

| Status | Body | Description |
| --- | --- | --- |
| 200 | PermissionPoliciesPage | Policy operation completed |
| 400 | ErrorResponse | Error response. A 403 from a permission check carries ForbiddenDetails in error.details. |
| 401 | ErrorResponse | Error response. A 403 from a permission check carries ForbiddenDetails in error.details. |
| 403 | ErrorResponse | Error response. A 403 from a permission check carries ForbiddenDetails in error.details. |
| 404 | ErrorResponse | Error response. A 403 from a permission check carries ForbiddenDetails in error.details. |
| 409 | ErrorResponse | Error response. A 403 from a permission check carries ForbiddenDetails in error.details. |

Response body (200):

- `name` · string · required
- `policies` · array<PermissionPolicy> · required
  - `scope_kind` · enum · required One of `organization`, `team`, `agent`.
  - `resource_ids` · array<string> · required: Empty for organization scope; otherwise the selected team IDs or agent slugs.
  - `permissions` · array<string> · required
  - `id` · uuid · required
  - `revision` · int64 · required
  - `resources` · array<PermissionResource> · required
    - `id` · string · required
    - `name` · string · required
    - `kind` · enum · required One of `organization`, `team`, `agent`.
  - `source` · enum · required One of `manual`, `directory`.
  - `via_team_id` · uuid
  - `via_team_name` · string
  - `can_edit` · boolean · required
  - `read_only_reason` · string
- `resources` · array<PermissionResource> · required
  - `id` · string · required
  - `name` · string · required
  - `kind` · enum · required One of `organization`, `team`, `agent`.
- `permissions` · array<PermissionDescriptor> · required
  - `key` · string · required: The permission key, shaped `resource.action`.
  - `area` · string · required: The key's first segment, for grouping.
  - `description` · string · required: What the permission allows.
  - `holders` · array<enum> · required: Who can hold it: people, agents, or the gateway.
  - `resource` · enum · required: What kind of thing a check on this permission is about. A grant must cover that thing's scope. One of `organization`, `team`, `agent`, `request`, `scope`.
  - `dangerous` · boolean · required: Whether granting or using it changes what other people can do, or cannot be undone. Such permissions need a fresh, strong sign-in to use and never arrive through directory sync or first SSO sign-in.
- `can_create` · boolean · required
- `is_owner` · boolean · required
- `read_only_reason` · string

### Example

```bash
curl -X GET "$WITHHUMAN_URL/api/v1/teams/{id}/permission_policies" \
  -H "Authorization: Bearer $WITHHUMAN_API_KEY"
```

200 response

```json
{
  "name": "string",
  "policies": [
    {
      "scope_kind": "organization",
      "resource_ids": [
        "string"
      ],
      "permissions": [
        "string"
      ],
      "id": "7ab8c8ec-7b2d-4fd6-9b52-752f9515eb71",
      "revision": 1,
      "resources": [
        {
          "id": "string",
          "name": "string",
          "kind": "organization"
        }
      ],
      "source": "manual",
      "via_team_id": "7ab8c8ec-7b2d-4fd6-9b52-752f9515eb71",
      "via_team_name": "string",
      "can_edit": true,
      "read_only_reason": "string"
    }
  ],
  "resources": [
    {
      "id": "string",
      "name": "string",
      "kind": "organization"
    }
  ],
  "permissions": [
    {
      "key": "request.decide",
      "area": "request",
      "description": "Approve or deny requests routed to you",
      "holders": [
        "human"
      ],
      "resource": "organization",
      "dangerous": true
    }
  ],
  "can_create": true,
  "is_owner": true,
  "read_only_reason": "string"
}
```

## Post teams permission policies

`POST /api/v1/teams/{id}/permission_policies`

Auth: Reviewer session cookie (`withhuman_session`), browser only; a personal API key is refused

Requires: `permission_policy.write`

### Request

| Parameter | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `id` | path | uuid | yes |  |
| `Idempotency-Key` | header | string | yes |  |

Request body:

- `scope_kind` · enum · required One of `organization`, `team`, `agent`.
- `resource_ids` · array<string> · required: Empty for organization scope; otherwise the selected team IDs or agent slugs.
- `permissions` · array<string> · required

### Response

| Status | Body | Description |
| --- | --- | --- |
| 201 | PermissionPolicy | Policy operation completed |
| 400 | ErrorResponse | Error response. A 403 from a permission check carries ForbiddenDetails in error.details. |
| 401 | ErrorResponse | Error response. A 403 from a permission check carries ForbiddenDetails in error.details. |
| 403 | ErrorResponse | Error response. A 403 from a permission check carries ForbiddenDetails in error.details. |
| 404 | ErrorResponse | Error response. A 403 from a permission check carries ForbiddenDetails in error.details. |
| 409 | ErrorResponse | Error response. A 403 from a permission check carries ForbiddenDetails in error.details. |

Response body (201):

- `scope_kind` · enum · required One of `organization`, `team`, `agent`.
- `resource_ids` · array<string> · required: Empty for organization scope; otherwise the selected team IDs or agent slugs.
- `permissions` · array<string> · required
- `id` · uuid · required
- `revision` · int64 · required
- `resources` · array<PermissionResource> · required
  - `id` · string · required
  - `name` · string · required
  - `kind` · enum · required One of `organization`, `team`, `agent`.
- `source` · enum · required One of `manual`, `directory`.
- `via_team_id` · uuid
- `via_team_name` · string
- `can_edit` · boolean · required
- `read_only_reason` · string

### Example

```bash
curl -X POST "$WITHHUMAN_URL/api/v1/teams/{id}/permission_policies" \
  -b "withhuman_session=$WITHHUMAN_SESSION" \
  -H "Idempotency-Key: string" \
  -H "Content-Type: application/json" \
  -d '{
  "scope_kind": "organization",
  "resource_ids": [
    "string"
  ],
  "permissions": [
    "string"
  ]
}'
```

201 response

```json
{
  "scope_kind": "organization",
  "resource_ids": [
    "string"
  ],
  "permissions": [
    "string"
  ],
  "id": "7ab8c8ec-7b2d-4fd6-9b52-752f9515eb71",
  "revision": 1,
  "resources": [
    {
      "id": "string",
      "name": "string",
      "kind": "organization"
    }
  ],
  "source": "manual",
  "via_team_id": "7ab8c8ec-7b2d-4fd6-9b52-752f9515eb71",
  "via_team_name": "string",
  "can_edit": true,
  "read_only_reason": "string"
}
```

## Get teams permission policy

`GET /api/v1/teams/{id}/permission_policies/{policy_id}`

Auth: Reviewer session cookie (`withhuman_session`) or personal API key (`Authorization: Bearer $WITHHUMAN_API_KEY`)

Self-service: acts on the caller's own account, no permission needed.

### Request

| Parameter | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `id` | path | uuid | yes |  |
| `policy_id` | path | uuid | yes |  |

### Response

| Status | Body | Description |
| --- | --- | --- |
| 200 | PermissionPolicy | Policy operation completed |
| 400 | ErrorResponse | Error response. A 403 from a permission check carries ForbiddenDetails in error.details. |
| 401 | ErrorResponse | Error response. A 403 from a permission check carries ForbiddenDetails in error.details. |
| 403 | ErrorResponse | Error response. A 403 from a permission check carries ForbiddenDetails in error.details. |
| 404 | ErrorResponse | Error response. A 403 from a permission check carries ForbiddenDetails in error.details. |
| 409 | ErrorResponse | Error response. A 403 from a permission check carries ForbiddenDetails in error.details. |

Response body (200):

- `scope_kind` · enum · required One of `organization`, `team`, `agent`.
- `resource_ids` · array<string> · required: Empty for organization scope; otherwise the selected team IDs or agent slugs.
- `permissions` · array<string> · required
- `id` · uuid · required
- `revision` · int64 · required
- `resources` · array<PermissionResource> · required
  - `id` · string · required
  - `name` · string · required
  - `kind` · enum · required One of `organization`, `team`, `agent`.
- `source` · enum · required One of `manual`, `directory`.
- `via_team_id` · uuid
- `via_team_name` · string
- `can_edit` · boolean · required
- `read_only_reason` · string

### Example

```bash
curl -X GET "$WITHHUMAN_URL/api/v1/teams/{id}/permission_policies/{policy_id}" \
  -H "Authorization: Bearer $WITHHUMAN_API_KEY"
```

200 response

```json
{
  "scope_kind": "organization",
  "resource_ids": [
    "string"
  ],
  "permissions": [
    "string"
  ],
  "id": "7ab8c8ec-7b2d-4fd6-9b52-752f9515eb71",
  "revision": 1,
  "resources": [
    {
      "id": "string",
      "name": "string",
      "kind": "organization"
    }
  ],
  "source": "manual",
  "via_team_id": "7ab8c8ec-7b2d-4fd6-9b52-752f9515eb71",
  "via_team_name": "string",
  "can_edit": true,
  "read_only_reason": "string"
}
```

## Put teams permission policy

`PUT /api/v1/teams/{id}/permission_policies/{policy_id}`

Auth: Reviewer session cookie (`withhuman_session`), browser only; a personal API key is refused

Requires: `permission_policy.write`

### Request

| Parameter | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `id` | path | uuid | yes |  |
| `policy_id` | path | uuid | yes |  |
| `Idempotency-Key` | header | string | yes |  |

Request body:

- `scope_kind` · enum · required One of `organization`, `team`, `agent`.
- `resource_ids` · array<string> · required: Empty for organization scope; otherwise the selected team IDs or agent slugs.
- `permissions` · array<string> · required
- `revision` · int64 · required: Required when updating an existing policy.

### Response

| Status | Body | Description |
| --- | --- | --- |
| 200 | PermissionPolicy | Policy operation completed |
| 400 | ErrorResponse | Error response. A 403 from a permission check carries ForbiddenDetails in error.details. |
| 401 | ErrorResponse | Error response. A 403 from a permission check carries ForbiddenDetails in error.details. |
| 403 | ErrorResponse | Error response. A 403 from a permission check carries ForbiddenDetails in error.details. |
| 404 | ErrorResponse | Error response. A 403 from a permission check carries ForbiddenDetails in error.details. |
| 409 | ErrorResponse | Error response. A 403 from a permission check carries ForbiddenDetails in error.details. |

Response body (200):

- `scope_kind` · enum · required One of `organization`, `team`, `agent`.
- `resource_ids` · array<string> · required: Empty for organization scope; otherwise the selected team IDs or agent slugs.
- `permissions` · array<string> · required
- `id` · uuid · required
- `revision` · int64 · required
- `resources` · array<PermissionResource> · required
  - `id` · string · required
  - `name` · string · required
  - `kind` · enum · required One of `organization`, `team`, `agent`.
- `source` · enum · required One of `manual`, `directory`.
- `via_team_id` · uuid
- `via_team_name` · string
- `can_edit` · boolean · required
- `read_only_reason` · string

### Example

```bash
curl -X PUT "$WITHHUMAN_URL/api/v1/teams/{id}/permission_policies/{policy_id}" \
  -b "withhuman_session=$WITHHUMAN_SESSION" \
  -H "Idempotency-Key: string" \
  -H "Content-Type: application/json" \
  -d '{
  "scope_kind": "organization",
  "resource_ids": [
    "string"
  ],
  "permissions": [
    "string"
  ],
  "revision": 1
}'
```

200 response

```json
{
  "scope_kind": "organization",
  "resource_ids": [
    "string"
  ],
  "permissions": [
    "string"
  ],
  "id": "7ab8c8ec-7b2d-4fd6-9b52-752f9515eb71",
  "revision": 1,
  "resources": [
    {
      "id": "string",
      "name": "string",
      "kind": "organization"
    }
  ],
  "source": "manual",
  "via_team_id": "7ab8c8ec-7b2d-4fd6-9b52-752f9515eb71",
  "via_team_name": "string",
  "can_edit": true,
  "read_only_reason": "string"
}
```

## Delete teams permission policy

`DELETE /api/v1/teams/{id}/permission_policies/{policy_id}`

Auth: Reviewer session cookie (`withhuman_session`), browser only; a personal API key is refused

Requires: `permission_policy.write`

### Request

| Parameter | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `id` | path | uuid | yes |  |
| `policy_id` | path | uuid | yes |  |
| `Idempotency-Key` | header | string | yes |  |
| `revision` | query | integer | yes |  |

### Response

| Status | Body | Description |
| --- | --- | --- |
| 204 |  | Policy operation completed |
| 400 | ErrorResponse | Error response. A 403 from a permission check carries ForbiddenDetails in error.details. |
| 401 | ErrorResponse | Error response. A 403 from a permission check carries ForbiddenDetails in error.details. |
| 403 | ErrorResponse | Error response. A 403 from a permission check carries ForbiddenDetails in error.details. |
| 404 | ErrorResponse | Error response. A 403 from a permission check carries ForbiddenDetails in error.details. |
| 409 | ErrorResponse | Error response. A 403 from a permission check carries ForbiddenDetails in error.details. |

### Example

```bash
curl -X DELETE "$WITHHUMAN_URL/api/v1/teams/{id}/permission_policies/{policy_id}?revision=1" \
  -b "withhuman_session=$WITHHUMAN_SESSION" \
  -H "Idempotency-Key: string"
```
