---
title: "Add role assignments"
method: POST
path: "/role-assignments"
tags: ["role-assignments"]
---

# Add role assignments

`POST /role-assignments`

## Request

- Base URL: `https://api.opal.dev/v1`
- URL: `https://api.opal.dev/v1/role-assignments`
- Auth: HTTP bearer

Grants one or more principals (user, group, or resource) access to one or more
entities (resource or group). The request body is batched to match the GraphQL
`addRoleAssignments` mutation, which this endpoint thin-wraps. The caller must
hold the `EDIT_ASSIGNMENTS` role permission on every entity in the batch.

Omit `access_level_remote_id` for entities that have a single default access
level. Omit `duration_minutes` (or send `0`) for an indefinite grant.

**Atomicity:** the batch is applied as a single transaction. If any entry
fails input validation, authorization, lookup, or creation, **no entries
are created** and the response identifies the first failing entry's
`entity_id` and `principal_id` (and the request-array index for input
validation errors). Clients should retry with the offending entry
removed; partial-success is intentionally not supported, matching the
semantics of the GraphQL `addRoleAssignments` mutation that this
endpoint wraps.

Prefer this endpoint for batch or programmatic use (e.g. Terraform). For a
single grant on a specific entity/principal pair, the per-pair endpoints —
`/resources/{id}/users/{id}`, `/groups/{id}/users/{id}`,
`/groups/{id}/resources/{id}`, `/groups/{id}/containing-groups`,
`/resources/{id}/non-human-identities/{id}` — are often simpler.

## Request body

- AddRoleAssignmentsRequest: Request body for `POST /role-assignments`.
  - `role_assignments` AddRoleAssignmentInput[], required: The list of role assignments to add.
    - `entity_id` string, uuid, required: The ID of the entity (resource or group) being accessed.
    - `entity_type` 'GROUP' | 'RESOURCE' | 'USER', required: The type of an entity.
    - `principal_id` string, uuid, required: The ID of the principal (user, group, or resource) being granted access.
    - `principal_type` 'GROUP' | 'RESOURCE' | 'USER', required: The type of an entity.
    - `access_level_remote_id` string, nullable: Remote ID of the access level to grant. Omit for entities that only have a single default access level.
    - `duration_minutes` integer, nullable: Duration of the grant in minutes. Omit or send `0` for an indefinite grant.

## Response `200`

The requested role assignments were created.

- AddRoleAssignmentsResponse: Response body for `POST /role-assignments`.
  - `task_ids` string[], required: Opaque IDs of push tasks enqueued to propagate the new assignments to end systems. Each ID is a base64-encoded, URL-safe string that mirrors the identifier returned by the GraphQL `addRoleAssignments` mutation.
  - `entries` AddRoleAssignmentsEntry[], required: One entry per role assignment created, in request order.
    - `role_assignment` RoleAssignment, required: A role assignment representing access granted to a principal for an entity.
      - `id` string, uuid, required: Unique identifier for the role assignment.
      - `entity_id` string, uuid, required: The ID of the entity (resource or group) being accessed.
      - `entity_type` 'GROUP' | 'RESOURCE' | 'USER', required: The type of an entity.
      - `principal_id` string, uuid, required: The ID of the principal being granted access.
      - `principal_type` 'GROUP' | 'RESOURCE' | 'USER', required: The type of an entity.
      - `access_level` ResourceAccessLevel: # Access Level Object ### Description The `AccessLevel` object is used to represent the level of access that a principal has. The "default" access level is a `AccessLevel` object whose fields are all empty strings. ### Usage Example View the `AccessLevel` of a resource/user or resource/group pair to see the level of access granted to the resource.
        - `access_level_name` string, required: The human-readable name of the access level.
        - `access_level_remote_id` string, required: The machine-readable identifier of the access level.
      - `expiration` string, date-time, nullable: When this access expires. Null when the grant is indefinite.
      - `granted_at` string, date-time, nullable: When this access was first granted.

## Other responses

- `400`: Invalid request body: unknown field, missing required field, malformed JSON, invalid enum (entity_type must be RESOURCE or GROUP; principal_type must be RESOURCE, GROUP, or USER), negative duration_minutes, or role_assignments batch size outside the allowed range.
- `403`: Caller is not authorized to edit role assignments on one or more of the entities.
- `404`: One or more referenced entities or principals does not exist in the organization.
- `409`: One of the requested role assignments already exists. The error message identifies the offending entity_id and principal_id so a batch client can locate the colliding entry. Matches the convention of the per-pair endpoints (`/resources/{id}/users/{id}`, `/groups/{id}/users/{id}`), which also return 409 for the same condition.
- `422`: Request was syntactically valid but could not be fulfilled. Includes group nesting cycles, self-grants, invalid durations, access-level mismatches, assignments on computed groups or non-requestable resources, and unsupported entity/principal type combinations.

## Changes

- **2026-10-16** `8c571c24985d`: 1 info
  - endpoint added

[Operation history](https://skmtc.dev/opal/apis/opal-api/changes/role-assignments/post.md)

---

[API](https://skmtc.dev/opal/apis/opal-api.md) - [All operations](https://skmtc.dev/opal/apis/opal-api/llms.txt) - [OpenAPI document](https://skmtc.dev/opal/apis/opal-api/revisions/e7877ea96eab?raw)
