Skip to content

Create a permission grant ​

POST/api/v2/workspaces/{slug}/permissions/grants/

Give a user or a teamspace a relation on one resource, optionally with extra permissions, refused permissions, and an expiry. The new access takes effect on the subject's next request.

A subject has at most one grant on a resource. If the subject already holds one, this request replaces it. The relation, both permission lists, and expires_at take the values you send. The grant keeps its id, and the response is still 201. A list you leave out becomes empty, and a missing expires_at becomes null.

You need the <resource_type>:manage permission on the resource. Otherwise the response is 403. See Who can manage grants.

Path Parameters ​

slug:requiredstring

The workspace slug. It appears in your Plane URLs. In https://app.plane.so/my-team/projects/, the slug is my-team.

Body Parameters ​

subject_type:requiredstring

Who receives the grant. user for one user, or teamspace for every member of a teamspace. Plane applies a teamspace grant to its members on project resources. Any other value returns 400.

subject_id:requiredstring (uuid)

The id of the user or teamspace. For a user, an id that matches no user returns 400 with Unknown user.

relation:requiredstring

The role the subject holds on the resource, as a role slug. On a project the system roles are admin, contributor, commenter, and guest. On a workspace they are owner, admin, member, and guest. A custom role uses its own slug. Maximum 50 characters.

Plane doesn't check the value against the workspace's roles. Read valid slugs from List roles.

resource_type:requiredstring

The kind of resource, as the permission engine names it, for example project, cycle, or module. Maximum 20 characters. A type Plane doesn't know returns 400 with Unknown resource type: <value>.

resource_id:requiredstring (uuid)

The id of the resource. It must belong to this workspace, or the response is 400 with That resource does not belong to this workspace. For resource_type=workspace, it must be this workspace's id.

permissions_grant:optionalarray of string

Extra permissions on this resource beyond what the relation gives. Each entry is a permission such as workitem:archive, or one of the wildcards * and <resource_type>:*. Each entry is at most 50 characters. Defaults to an empty list.

An entry that isn't a real permission returns 400, and the error message lists the invalid entries.

permissions_deny:optionalarray of string

Permissions refused on this resource, even when the relation, permissions_grant, or a parent's role would allow them. Same format and validation as permissions_grant. Defaults to an empty list.

expires_at:optionalstring (date-time)

When the grant expires. Omit it or send null for a grant that never expires.

Response shaping ​

fields:optionalstring

Comma-separated list of fields to return. Unrequested keys are omitted from the response, not returned as null, so absent means "not requested" and null means "actually null". id always comes back whether or not you name it.

Pass all for every requestable field. An unknown name is a 400 that lists the valid set and suggests the closest match, so a typo can't silently cost you the saving.

Requestable here: created_at, expires_at, id, permissions_deny, permissions_grant, relation, resource_id, resource_type, subject_id, subject_type, updated_at.

See Sparse fields.

Scopes ​

workspaces.permissions:write

Errors ​

StatusCodeCause
400invalid_requestA required field is missing, subject_type is unknown, the user doesn't exist, the resource type is unknown or the resource is in another workspace, or a permission string is invalid.
401unauthorizedMissing or invalid credentials.
402payment_requiredThe feature this endpoint belongs to isn't enabled on your plan, or is switched off.
403forbiddenYou can't manage access to this resource, or your token scope doesn't allow this.
404not_foundNo such workspace.
406not_acceptableThe Accept header asks for a representation the API can't produce.
409conflictNot raised by this endpoint's own checks. A repeated create replaces the existing grant instead.
413payload_too_largeThe request body is over the size limit.
415unsupported_media_typeThe Content-Type isn't one this endpoint accepts.
429rate_limitedThrottled. Honor the Retry-After header before retrying.
Create a permission grant
bash
curl -X POST \
  "https://api.plane.so/api/v2/workspaces/my-team/permissions/grants/" \
  -H "X-Api-Key: $PLANE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "subject_type": "user",
  "subject_id": "a8d3e5f1-6b2c-4e97-8d40-1c5f7a9b3e26",
  "relation": "guest",
  "resource_type": "project",
  "resource_id": "4af68566-94a4-4eb3-94aa-50dc9427067b",
  "permissions_grant": ["workitem:create"],
  "expires_at": "2026-03-31T23:59:59Z"
}'
Response201
json
{
  "id": "e2f9c4a7-1b8d-4c63-9e05-7a3b6d1f8c94",
  "subject_type": "user",
  "subject_id": "a8d3e5f1-6b2c-4e97-8d40-1c5f7a9b3e26",
  "relation": "guest",
  "resource_type": "project",
  "resource_id": "4af68566-94a4-4eb3-94aa-50dc9427067b",
  "permissions_grant": ["workitem:create"],
  "permissions_deny": [],
  "expires_at": "2026-03-31T23:59:59Z",
  "created_at": "2026-01-20T11:05:17.204918Z",
  "updated_at": "2026-01-20T11:05:17.204918Z"
}
Response400
json
{
  "type": "invalid_request",
  "code": "invalid_request",
  "detail": "One or more fields failed validation.",
  "errors": [
    {
      "field": "permissions_grant",
      "code": "invalid",
      "message": "Invalid permission strings: workitem:teleport"
    }
  ]
}
Response400
json
{
  "type": "invalid_request",
  "code": "invalid_request",
  "detail": "That resource does not belong to this workspace."
}