Create a project label
Add a label to the workspace's project label catalog. Only workspace owners and admins can create project labels.
Names are unique in the workspace without regard to case. If the name is taken, the 409 conflict body carries the existing label's id, so an importer can use that label instead.
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
name:requiredstringDisplay name. Maximum 255 characters. A name already in use, in any letter case, returns 409 conflict.
color:optionalstringHex color used wherever the label is rendered, for example #e5484d. Maximum 255 characters. Can be empty.
description:optionalstringFree-form note about what the label is for. Can be empty.
sort_order:optionalnumberOrdering weight. Lower values sort first. Plane places a new label after the last one and uses your value only when the workspace has no project labels. To reposition a label, send sort_order in Update a project label.
Response shaping
fields:optionalstringComma-separated list of fields to return. Fields you don't request are left out of the response instead of coming back as null, so a missing key means you didn't ask for it and null means the value is null. id is always included, whether or not you name it.
Pass all to get every requestable field. An unknown field name returns 400 with the list of valid names and the closest match, so a typo is caught instead of being silently ignored.
Requestable here: color, created_at, created_by_id, description, id, name, sort_order, updated_at, updated_by_id.
See Sparse fields.
Scopes
workspaces.project_labels:write
Errors
| Status | Code | Cause |
|---|---|---|
400 | invalid_request | Missing name, or a field over its length limit. |
401 | unauthorized | Missing or invalid credentials. |
403 | forbidden | Only workspace owners and admins can create project labels, or your token lacks the scope. |
404 | not_found | No such workspace, or it's outside your tenant. |
406 | not_acceptable | The Accept header asks for a representation the API can't produce. |
409 | conflict | A project label with this name, in any letter case, already exists. The body carries its id. |
409 | governance_migration_in_progress | A governance migration is running, so the workspace is read-only. Retry after it finishes. |
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/project-labels/" \
-H "X-Api-Key: $PLANE_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Customer-facing",
"color": "#e5484d",
"description": "Projects that ship changes customers see"
}'import requests
response = requests.post(
"https://api.plane.so/api/v2/workspaces/my-team/project-labels/",
headers={"X-Api-Key": "your-api-key"},
json={
"name": "Customer-facing",
"color": "#e5484d",
"description": "Projects that ship changes customers see",
},
)
print(response.json())const response = await fetch("https://api.plane.so/api/v2/workspaces/my-team/project-labels/", {
method: "POST",
headers: {
"X-Api-Key": "your-api-key",
"Content-Type": "application/json",
},
body: JSON.stringify({
name: "Customer-facing",
color: "#e5484d",
description: "Projects that ship changes customers see",
}),
});
const data = await response.json();{
"id": "71c3e8a9-4d2f-4b60-9e17-5a8c0b3f2d94",
"name": "Customer-facing",
"description": "Projects that ship changes customers see",
"color": "#e5484d",
"sort_order": 85535,
"created_at": "2026-02-03T16:45:12.904117Z",
"updated_at": "2026-02-03T16:45:12.904117Z",
"created_by_id": "16c61a3a-512a-48ac-b0be-b6b46fe6f430",
"updated_by_id": null
}{
"type": "conflict",
"code": "conflict",
"detail": "A project label with this name already exists in this workspace.",
"id": "d8a4f2c6-1e7b-4093-9c5a-6b2e0f8d3a71"
}
