Create a permission grant
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:requiredstringThe 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:requiredstringWho 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:requiredstringThe 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:requiredstringThe 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 stringExtra 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 stringPermissions 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:optionalstringComma-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
| Status | Code | Cause |
|---|---|---|
400 | invalid_request | A 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. |
401 | unauthorized | Missing or invalid credentials. |
402 | payment_required | The feature this endpoint belongs to isn't enabled on your plan, or is switched off. |
403 | forbidden | You can't manage access to this resource, or your token scope doesn't allow this. |
404 | not_found | No such workspace. |
406 | not_acceptable | The Accept header asks for a representation the API can't produce. |
409 | conflict | Not raised by this endpoint's own checks. A repeated create replaces the existing grant instead. |
413 | payload_too_large | The request body is over the size limit. |
415 | unsupported_media_type | The Content-Type isn't one this endpoint accepts. |
429 | rate_limited | Throttled. Honor the Retry-After header before retrying. |
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"
}'import requests
response = requests.post(
"https://api.plane.so/api/v2/workspaces/my-team/permissions/grants/",
headers={"X-Api-Key": "your-api-key"},
json={
"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",
},
)
print(response.json())const response = await fetch("https://api.plane.so/api/v2/workspaces/my-team/permissions/grants/", {
method: "POST",
headers: {
"X-Api-Key": "your-api-key",
"Content-Type": "application/json",
},
body: JSON.stringify({
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",
}),
});
const data = await response.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"
}{
"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"
}
]
}{
"type": "invalid_request",
"code": "invalid_request",
"detail": "That resource does not belong to this workspace."
}
