Cast a vote on a work item
Set your vote on a work item to an upvote or a downvote. The vote belongs to the caller. You can't cast a vote for another user.
If you have already voted on the work item, the request changes that vote in place. The response has the same id and the new vote. Sending the same value again changes only updated_at. The response is 200 with your vote in every case, including your first vote on the work item.
You need the workitem:react permission on the work item, or the response is 403. Project admins, contributors, and commenters have it. Guests can't vote. A work item you can't see returns 404. By default, an archived work item also returns 404. Pass ?archived=true to reach one.
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.
project_id:requiredstringThe project the work item belongs to. Accepts the project UUID or its identifier, for example ENG.
pk:requiredstringThe work item. Accepts its UUID or its human identifier, for example ENG-142.
Body parameters
vote:requiredinteger1 for an upvote or -1 for a downvote. Any other value returns 400.
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: actor_id, created_at, id, project_id, updated_at, vote, work_item_id.
See Sparse fields.
expand:optionalstringComma-separated relations to embed alongside the ids: actor (you, as the voter, with id, display_name, avatar_url, and email). An unknown value returns 400. See Expanding relations.
Scopes
projects.work_items:write
Errors
| Status | Code | Cause |
|---|---|---|
400 | invalid_request | vote is missing, or isn't 1 or -1. |
401 | unauthorized | Missing or invalid credentials. |
403 | forbidden | You can't react to this work item, or your token scope doesn't allow this. |
404 | not_found | No such work item, project, or workspace, you can't see the work item, or it's archived and you didn't pass ?archived=true. |
406 | not_acceptable | The Accept header asks for a representation the API can't produce. |
409 | governance_migration_in_progress | A governance migration is running in this workspace, so writes are paused. 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/projects/4af68566-94a4-4eb3-94aa-50dc9427067b/work-items/8c3d1f72-4a9e-4b65-9d20-6e1f3b7a5c48/votes/" \
-H "X-Api-Key: $PLANE_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"vote": 1
}'import requests
response = requests.post(
"https://api.plane.so/api/v2/workspaces/my-team/projects/4af68566-94a4-4eb3-94aa-50dc9427067b/work-items/8c3d1f72-4a9e-4b65-9d20-6e1f3b7a5c48/votes/",
headers={"X-Api-Key": "your-api-key"},
json={"vote": 1},
)
print(response.json())const response = await fetch(
"https://api.plane.so/api/v2/workspaces/my-team/projects/4af68566-94a4-4eb3-94aa-50dc9427067b/work-items/8c3d1f72-4a9e-4b65-9d20-6e1f3b7a5c48/votes/",
{
method: "POST",
headers: {
"X-Api-Key": "your-api-key",
"Content-Type": "application/json",
},
body: JSON.stringify({ vote: 1 }),
},
);
const data = await response.json();{
"id": "d6a2f8c1-3e7b-4a95-8c04-9f1e5b2d7a63",
"work_item_id": "8c3d1f72-4a9e-4b65-9d20-6e1f3b7a5c48",
"project_id": "4af68566-94a4-4eb3-94aa-50dc9427067b",
"actor_id": "16c61a3a-512a-48ac-b0be-b6b46fe6f430",
"vote": 1,
"created_at": "2026-01-14T09:22:41.478363Z",
"updated_at": "2026-01-14T09:22:41.478363Z"
}{
"type": "invalid_request",
"code": "invalid_request",
"detail": "One or more fields failed validation.",
"errors": [
{
"field": "vote",
"code": "invalid_choice",
"message": "\"5\" is not a valid choice."
}
]
}
