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

> ## Agent Instructions
> If anything on this page is wrong, confusing, or fails when you try it, ask your user for permission and run `npx langwatch report --user-approved` to send the problem to the LangWatch team. No login is needed and secrets are redacted locally. `npx langwatch report --help` has the details.

# Overview

> Deprecated: use the Grants API. Grant a role to one principal, a user, a group or an API key, at one scope: the organization, a team, or a single project.

> [!NOTE]
>
> **Available on Enterprise plans.** An organization without an Enterprise plan is refused with HTTP
> 402 and the error code `enterprise_plan_required`. Self-hosted deployments need an Enterprise
> license for the same endpoints. To enable it, reach out to
> [enterprise@langwatch.ai](mailto:enterprise@langwatch.ai).

> [!WARNING]
>
> **Deprecated.** The Role Bindings API is superseded by the [Grants API](https://langwatch.ai/docs/api-reference/grants/overview.md).
> It keeps working, but every answer carries `Deprecation: true` and a `Link` header naming
> `/api/grants` as its successor. New integrations should use grants; see [Migrating to
> grants](#migrating-to-grants).

## Intro

A role binding is one sentence: **this principal has this role, here**. It is how every kind of access in LangWatch is expressed, so this is the API to read when you want to know who can reach what.

Each binding names exactly one principal, and there are three kinds:

| Principal | Field | Used for |
| - | - | - |
| A person | `userId` | Access someone has directly, on top of anything their groups give them |
| A group | `groupId` | Access every member of the group inherits, including groups synced from your identity provider |
| An API key | `apiKeyId` | What a service credential may reach, independent of any person |

## Authentication

Requires an **organization-level API key** with `organization:manage`. Pass it as a Bearer token:

```http theme={null}
Authorization: Bearer sk-lw-<id>_<secret>
```

## Endpoints

| Method | Path | Description |
| - | - | - |
| `GET` | `/api/v1/role-bindings` | List bindings, filtered by principal or scope |
| `POST` | `/api/v1/role-bindings` | Grant one role to one principal at one scope |
| `PATCH` | `/api/v1/role-bindings/{id}` | Change the role a binding grants |
| `DELETE` | `/api/v1/role-bindings/{id}` | Delete a binding |

`GET` filters on `userId`, `groupId`, `apiKeyId`, `scopeType` and `scopeId`, and pages with `offset` and `limit`. `limit` defaults to 50 and cannot exceed 200. `totalCount` counts the filtered set, not the organization.

## Scopes

| `scopeType` | `scopeId` | What it reaches |
| - | - | - |
| `ORGANIZATION` | the organization id | Every team and project in the organization |
| `TEAM` | a team id | That team and all its projects |
| `PROJECT` | a project id | That project only |

The roles are `ADMIN`, `MEMBER`, `VIEWER`, and `CUSTOM`. `CUSTOM` carries a `customRoleId` from the [Roles](https://langwatch.ai/docs/api-reference/roles/overview.md) API, and is refused without one (`custom_role_id_required`).

Some permissions only mean something for the organization as a whole, membership and billing among them. Granting one through a team- or project-scoped binding is refused at write time with `org_exclusive_permission_scope`, rather than being accepted and quietly having no effect.

## Creating twice makes two bindings

Each create makes its own binding with its own id, even when the same principal already holds the same role at the same scope. A provisioning tool that must not duplicate should list first, or move to the [Grants API](https://langwatch.ai/docs/api-reference/grants/overview.md), whose create takes an `Idempotency-Key`. A `PATCH` changes the role; the principal and the scope stay as they are, so moving a grant somewhere else means a new binding.

You cannot bind a role carrying a permission you do not hold yourself at that scope, and a request made with an API key is judged by the key's own permissions. The refusal is 403 `grant_exceeds_caller_permissions`, with the permissions you lack in `meta.missingPermissions`. See [Roles & access](https://langwatch.ai/docs/platform/roles-and-access.md#you-cannot-grant-more-than-you-hold).

```bash theme={null}
# A group of engineers gets MEMBER on one team
curl -X POST https://app.langwatch.ai/api/v1/role-bindings \
  -H "Authorization: Bearer sk-lw-..." \
  -H "Content-Type: application/json" \
  -d '{
    "groupId": "group_abc123",
    "role": "MEMBER",
    "scopeType": "TEAM",
    "scopeId": "team_abc123"
  }'

# A CI key gets a narrow custom role on one project
curl -X POST https://app.langwatch.ai/api/v1/role-bindings \
  -H "Authorization: Bearer sk-lw-..." \
  -H "Content-Type: application/json" \
  -d '{
    "apiKeyId": "<api_key_id>",
    "role": "CUSTOM",
    "customRoleId": "<custom_role_id>",
    "scopeType": "PROJECT",
    "scopeId": "project_abc123"
  }'
```

## `hasLegacyAccessNotice`

A create response can carry `hasLegacyAccessNotice: true`. It means this is the person's first explicit binding, and until now their access came from plain team membership, which this write switches off. No call failed and no call is blocked: it is there so a tool can tell an operator that this user's access is now described entirely by bindings.

## Errors

| Code | Status | Meaning |
| - | - | - |
| `role_binding_principal_invalid` | 422 | Not exactly one of `userId`, `groupId`, `apiKeyId` |
| `user_not_in_organization` | 422 | The user is not a member here |
| `group_not_in_organization` | 422 | The group belongs to another organization |
| `api_key_not_in_organization` | 422 | The API key belongs to another organization |
| `scope_not_in_organization` | 422 | The team or project belongs to another organization |
| `custom_role_id_required` | 422 | Role is `CUSTOM` with no `customRoleId` |
| `custom_role_not_assignable` | 422 | That custom role cannot be granted here |
| `org_exclusive_permission_scope` | 422 | The role carries organization-only permissions, and the scope is a team or project |
| `grant_exceeds_caller_permissions` | 403 | The role carries permissions you do not hold at that scope |
| `grant_limit_reached` | 409 | The organization holds as many grants as it may |
| `cannot_remove_last_admin` | 400 | The binding is the organization's last administrator |
| `cannot_demote_last_admin` | 400 | The change would leave the organization without an administrator |
| `role_binding_not_found` | 404 | No such binding in this organization |

## Migrating to grants

Grants and role bindings are the same records, so existing bindings appear in `GET /api/v1/grants` straight away and no copying is needed. Change the calls:

| Role bindings | Grants |
| - | - |
| `GET /api/v1/role-bindings?userId=u1` | `GET /api/v1/grants?principalType=user&principalId=u1` |
| `offset` and `limit`, with `totalCount` | `cursor` and `limit`, with `nextCursor` |
| `{ "userId": "u1" }`, `groupId` or `apiKeyId` | `{ "principal": { "type": "user", "id": "u1" } }` |
| `"role": "MEMBER"` | `"roleId": "member"` |
| `"role": "CUSTOM", "customRoleId": "r1"` | `"roleId": "r1"` |
| `"scopeType": "TEAM", "scopeId": "t1"` | `"scope": { "type": "team", "id": "t1" }` |
| `PATCH /api/v1/role-bindings/{id}` with `role` | `PATCH /api/v1/grants/{grantId}` with `roleId` |
| `DELETE /api/v1/role-bindings/{id}` | `DELETE /api/v1/grants/{grantId}` |
| `langwatch role-bindings ...` | `langwatch grants list, get, create, change-role, revoke` |

Grants also add an optional `expiresAt`, a `status` filter, and an `Idempotency-Key` on create.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.