---
**Acting as a user (`X-PS-Impersonate-User`).** Every endpoint accepts an optional `X-PS-Impersonate-User` request header whose value is the email of the instance user to act as. The effective user resolves in this order: the header if present, else the instance's configured default MCP user, else none. User-scoped paths additionally accept the literal `me` in place of an email, and Graph-style apps also serve `/me/…` twins of their `/users/{email}/…` paths that resolve the same way. This header is honored by all operations but is intentionally omitted from each operation's parameter list so it is not surfaced as a per-tool argument by MCP clients; see the `ImpersonateUser` entry under `components.parameters`.
---
**Acting as a user (`X-PS-Impersonate-User`).** Every endpoint accepts an optional `X-PS-Impersonate-User` request header whose value is the email of the instance user to act as. The effective user resolves in this order: the header if present, else the instance's configured default MCP user, else none. User-scoped paths additionally accept the literal `me` in place of an email, and Graph-style apps also serve `/me/…` twins of their `/users/{email}/…` paths that resolve the same way. This header is honored by all operations but is intentionally omitted from each operation's parameter list so it is not surfaced as a per-tool argument by MCP clients; see the `ImpersonateUser` entry under `components.parameters`.
Operations
GET /projects
List projects
Purple Planner projects are the delivery workstreams that hold tasks, sprints and milestones, each with an owner, status, dates, progress and budget. This is project delivery, not a CRM account or an ERP cost centre. Filter/sort server-side with $filter, $orderby, $select, $search, $top, $skip and $count (MCP clients send these as _filter, _orderby, …) rather than listing everything and filtering locally.
Authentication
[
{
"patAuth": []
}
]
Request, responses and operation details
{
"summary": "List projects",
"description": "Purple Planner projects are the delivery workstreams that hold tasks, sprints and milestones, each with an owner, status, dates, progress and budget. This is project delivery, not a CRM account or an ERP cost centre. Filter/sort server-side with $filter, $orderby, $select, $search, $top, $skip and $count (MCP clients send these as _filter, _orderby, …) rather than listing everything and filtering locally.",
"parameters": [
{
"$ref": "#/components/parameters/InstanceId"
},
{
"in": "query",
"name": "limit",
"schema": {
"type": "integer",
"default": 50
},
"description": "Legacy page size (default 50). Prefer $top. Ignored when $top/$skip are used."
},
{
"in": "query",
"name": "cursor",
"schema": {
"type": "string"
},
"description": "Legacy keyset cursor — pass the previous response's nextCursor. Prefer $skip/$top."
},
{
"in": "query",
"name": "$filter",
"schema": {
"type": "string"
},
"description": "OData filter expression. Comparison operators: eq, ne, gt, ge, lt, le. Logical: and, or, not, with parentheses for grouping. String functions: contains(field,'x'), startswith(field,'x'), endswith(field,'x'). Membership: field in ('a','b'). Case-insensitive matching via tolower(field) / toupper(field). Nested fields use a slash, e.g. pto_balance/vacation_days. String literals use single quotes; dates are ISO-8601. Examples: \"status eq 'active'\"; \"salary gt 50000 and department eq 'Engineering'\"; \"contains(tolower(name),'acme')\"; \"closeDate gt 2026-01-01\"."
},
{
"in": "query",
"name": "$select",
"schema": {
"type": "string"
},
"description": "Comma-separated list of fields to return, e.g. \"id,firstName,email\". Nested fields via slash (\"pto_balance/vacation_days\"). The id field is always included."
},
{
"in": "query",
"name": "$orderby",
"schema": {
"type": "string"
},
"description": "Sort clause(s), comma-separated, each \"field [asc|desc]\" (asc is the default). Example: \"createdAt desc, lastName asc\". Null values sort last."
},
{
"in": "query",
"name": "$search",
"schema": {
"type": "string"
},
"description": "Free-text, case-insensitive substring match across the resource's searchable fields."
},
{
"in": "query",
"name": "$top",
"schema": {
"type": "integer"
},
"description": "Maximum number of items to return (page size)."
},
{
"in": "query",
"name": "$skip",
"schema": {
"type": "integer"
},
"description": "Number of matching items to skip before returning results (offset paging). Combine with $top."
},
{
"in": "query",
"name": "$count",
"schema": {
"type": "boolean"
},
"description": "When true, include @odata.count (total matches after $filter/$search, before paging) in the response."
}
],
"responses": {
"200": {
"description": "Paginated list",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"data": {
"type": "array",
"items": {
"$ref": "#/components/schemas/Project"
}
},
"nextCursor": {
"type": "string",
"nullable": true,
"description": "Cursor for the next page, or null on the last page. With $top/$skip this is the next $skip value; otherwise a keyset cursor."
},
"total": {
"type": "integer",
"description": "Total documents in the collection."
},
"@odata.count": {
"type": "integer",
"description": "Present only when $count=true: total matches after $filter/$search."
},
"@odata.truncated": {
"type": "boolean",
"description": "Present and true only when the collection exceeded the in-memory query cap and results may be incomplete."
}
}
}
}
}
},
"400": {
"description": "Invalid OData query (e.g. malformed $filter)"
}
}
}
POST /projects
Create project
Open a new delivery workstream in Purple Planner. Only for genuinely new projects — to change an existing one use the update-project tool, and to file a unit of work use create-task instead.
Authentication
[
{
"patAuth": []
}
]
Request, responses and operation details
{
"summary": "Create project",
"description": "Open a new delivery workstream in Purple Planner. Only for genuinely new projects — to change an existing one use the update-project tool, and to file a unit of work use create-task instead.",
"parameters": [
{
"$ref": "#/components/parameters/InstanceId"
}
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Project"
}
}
}
},
"responses": {
"201": {
"description": "Created",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Project"
}
}
}
}
}
}
GET /projects/{id}
Get project
Fetch one Purple Planner project by id (e.g. PROJ-001) with its owner, status, dates, progress, budget and the initiatives/teams it rolls up to.
Authentication
[
{
"patAuth": []
}
]
Request, responses and operation details
{
"summary": "Get project",
"description": "Fetch one Purple Planner project by id (e.g. PROJ-001) with its owner, status, dates, progress, budget and the initiatives/teams it rolls up to.",
"parameters": [
{
"$ref": "#/components/parameters/InstanceId"
},
{
"in": "path",
"name": "id",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "Found",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Project"
}
}
}
},
"404": {
"description": "Not found"
}
}
}
PATCH /projects/{id}
Update project
Partially update a Purple Planner project — status, owner, dates, progress, budget, initiativeIds or teamIds. Status changes are state-machine checked: a completed project cannot be reactivated (422).
Authentication
[
{
"patAuth": []
}
]
Request, responses and operation details
{
"summary": "Update project",
"description": "Partially update a Purple Planner project — status, owner, dates, progress, budget, initiativeIds or teamIds. Status changes are state-machine checked: a completed project cannot be reactivated (422).",
"parameters": [
{
"$ref": "#/components/parameters/InstanceId"
},
{
"in": "path",
"name": "id",
"required": true,
"schema": {
"type": "string"
}
}
],
"requestBody": {
"description": "Fields to update (partial)",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Project"
}
}
}
},
"responses": {
"200": {
"description": "Updated",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Project"
}
}
}
},
"404": {
"description": "Not found"
},
"422": {
"description": "State machine violation — e.g. cannot reactivate a completed project"
}
}
}
DELETE /projects/{id}
Delete project
Permanently delete a Purple Planner project. Destructive and irreversible, and its tasks/sprints keep a dangling projectId — prefer setting status to completed or cancelled with the update-project tool unless the record was created in error.
Authentication
[
{
"patAuth": []
}
]
Request, responses and operation details
{
"summary": "Delete project",
"description": "Permanently delete a Purple Planner project. Destructive and irreversible, and its tasks/sprints keep a dangling projectId — prefer setting status to completed or cancelled with the update-project tool unless the record was created in error.",
"parameters": [
{
"$ref": "#/components/parameters/InstanceId"
},
{
"in": "path",
"name": "id",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "Deleted",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"success": {
"type": "boolean"
}
}
}
}
}
},
"404": {
"description": "Not found"
}
}
}
GET /tasks
List tasks
Purple Planner tasks (issues) are the unit of work inside a project, carrying assignee, priority, story points, sprint, labels, dependencies and blockers. These are delivery issues, not ITSM incidents/service requests and not HR cases. Filter/sort server-side with $filter, $orderby, $select, $search, $top, $skip and $count (MCP clients send these as _filter, _orderby, …) rather than listing everything and filtering locally.
Authentication
[
{
"patAuth": []
}
]
Request, responses and operation details
{
"summary": "List tasks",
"description": "Purple Planner tasks (issues) are the unit of work inside a project, carrying assignee, priority, story points, sprint, labels, dependencies and blockers. These are delivery issues, not ITSM incidents/service requests and not HR cases. Filter/sort server-side with $filter, $orderby, $select, $search, $top, $skip and $count (MCP clients send these as _filter, _orderby, …) rather than listing everything and filtering locally.",
"parameters": [
{
"$ref": "#/components/parameters/InstanceId"
},
{
"in": "query",
"name": "limit",
"schema": {
"type": "integer",
"default": 50
},
"description": "Legacy page size (default 50). Prefer $top. Ignored when $top/$skip are used."
},
{
"in": "query",
"name": "cursor",
"schema": {
"type": "string"
},
"description": "Legacy keyset cursor — pass the previous response's nextCursor. Prefer $skip/$top."
},
{
"in": "query",
"name": "$filter",
"schema": {
"type": "string"
},
"description": "OData filter expression. Comparison operators: eq, ne, gt, ge, lt, le. Logical: and, or, not, with parentheses for grouping. String functions: contains(field,'x'), startswith(field,'x'), endswith(field,'x'). Membership: field in ('a','b'). Case-insensitive matching via tolower(field) / toupper(field). Nested fields use a slash, e.g. pto_balance/vacation_days. String literals use single quotes; dates are ISO-8601. Examples: \"status eq 'active'\"; \"salary gt 50000 and department eq 'Engineering'\"; \"contains(tolower(name),'acme')\"; \"closeDate gt 2026-01-01\"."
},
{
"in": "query",
"name": "$select",
"schema": {
"type": "string"
},
"description": "Comma-separated list of fields to return, e.g. \"id,firstName,email\". Nested fields via slash (\"pto_balance/vacation_days\"). The id field is always included."
},
{
"in": "query",
"name": "$orderby",
"schema": {
"type": "string"
},
"description": "Sort clause(s), comma-separated, each \"field [asc|desc]\" (asc is the default). Example: \"createdAt desc, lastName asc\". Null values sort last."
},
{
"in": "query",
"name": "$search",
"schema": {
"type": "string"
},
"description": "Free-text, case-insensitive substring match across the resource's searchable fields."
},
{
"in": "query",
"name": "$top",
"schema": {
"type": "integer"
},
"description": "Maximum number of items to return (page size)."
},
{
"in": "query",
"name": "$skip",
"schema": {
"type": "integer"
},
"description": "Number of matching items to skip before returning results (offset paging). Combine with $top."
},
{
"in": "query",
"name": "$count",
"schema": {
"type": "boolean"
},
"description": "When true, include @odata.count (total matches after $filter/$search, before paging) in the response."
}
],
"responses": {
"200": {
"description": "Paginated list",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"data": {
"type": "array",
"items": {
"$ref": "#/components/schemas/Task"
}
},
"nextCursor": {
"type": "string",
"nullable": true,
"description": "Cursor for the next page, or null on the last page. With $top/$skip this is the next $skip value; otherwise a keyset cursor."
},
"total": {
"type": "integer",
"description": "Total documents in the collection."
},
"@odata.count": {
"type": "integer",
"description": "Present only when $count=true: total matches after $filter/$search."
},
"@odata.truncated": {
"type": "boolean",
"description": "Present and true only when the collection exceeded the in-memory query cap and results may be incomplete."
}
}
}
}
}
}
}
}
POST /tasks
Create task
File a new task (issue) in a Purple Planner project, optionally with assignee, priority, story points, sprint and labels. Returns 422 if the target project is already completed or cancelled.
Authentication
[
{
"patAuth": []
}
]
Request, responses and operation details
{
"summary": "Create task",
"description": "File a new task (issue) in a Purple Planner project, optionally with assignee, priority, story points, sprint and labels. Returns 422 if the target project is already completed or cancelled.",
"parameters": [
{
"$ref": "#/components/parameters/InstanceId"
}
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Task"
}
}
}
},
"responses": {
"201": {
"description": "Created",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Task"
}
}
}
},
"422": {
"description": "State machine violation — e.g. cannot add a task to a completed or cancelled project"
}
}
}
GET /tasks/{id}
Get task
Fetch one Purple Planner task (issue) by id with its status, assignee, sprintId, labels, dependencies, blockers and openSubtasks — use it to check whether a task is actually completable.
Authentication
[
{
"patAuth": []
}
]
Request, responses and operation details
{
"summary": "Get task",
"description": "Fetch one Purple Planner task (issue) by id with its status, assignee, sprintId, labels, dependencies, blockers and openSubtasks — use it to check whether a task is actually completable.",
"parameters": [
{
"$ref": "#/components/parameters/InstanceId"
},
{
"in": "path",
"name": "id",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "Found",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Task"
}
}
}
},
"404": {
"description": "Not found"
}
}
}
PATCH /tasks/{id}
Update task
Partially update a Purple Planner task — retitle, reassign, reprioritise, move it to another sprint, or change status. Completion is guarded: moving to done/closed fails with 422 while openSubtasks > 0 or blockers is non-empty, and reopening a cancelled task requires reopenReason. To record that this task waits on another, use the add-dependency tool instead.
Authentication
[
{
"patAuth": []
}
]
Request, responses and operation details
{
"summary": "Update task",
"description": "Partially update a Purple Planner task — retitle, reassign, reprioritise, move it to another sprint, or change status. Completion is guarded: moving to done/closed fails with 422 while openSubtasks > 0 or blockers is non-empty, and reopening a cancelled task requires reopenReason. To record that this task waits on another, use the add-dependency tool instead.",
"parameters": [
{
"$ref": "#/components/parameters/InstanceId"
},
{
"in": "path",
"name": "id",
"required": true,
"schema": {
"type": "string"
}
}
],
"requestBody": {
"description": "Fields to update (partial)",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Task"
}
}
}
},
"responses": {
"200": {
"description": "Updated",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Task"
}
}
}
},
"404": {
"description": "Not found"
},
"422": {
"description": "State machine violation — e.g. task has open subtasks or active blockers preventing completion, or reopenReason missing when reopening a cancelled task"
}
}
}
DELETE /tasks/{id}
Delete task
Permanently delete a Purple Planner task. Destructive and irreversible — to finish work normally set status to done/closed with the update-task tool; only delete issues filed by mistake.
Authentication
[
{
"patAuth": []
}
]
Request, responses and operation details
{
"summary": "Delete task",
"description": "Permanently delete a Purple Planner task. Destructive and irreversible — to finish work normally set status to done/closed with the update-task tool; only delete issues filed by mistake.",
"parameters": [
{
"$ref": "#/components/parameters/InstanceId"
},
{
"in": "path",
"name": "id",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "Deleted",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"success": {
"type": "boolean"
}
}
}
}
}
},
"404": {
"description": "Not found"
}
}
}
POST /tasks/{id}/add-dependency
Add a task dependency
Record that this Purple Planner task is blocked until dependency_id finishes, appending to its dependencies array. Use this rather than patching the task, because the server rejects (400) any edge that would create a circular dependency.
Authentication
[
{
"patAuth": []
}
]
Request, responses and operation details
{
"summary": "Add a task dependency",
"description": "Record that this Purple Planner task is blocked until dependency_id finishes, appending to its dependencies array. Use this rather than patching the task, because the server rejects (400) any edge that would create a circular dependency.",
"parameters": [
{
"$ref": "#/components/parameters/InstanceId"
},
{
"in": "path",
"name": "id",
"required": true,
"schema": {
"type": "string"
}
}
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"dependency_id": {
"type": "string",
"example": "TASK-0002",
"description": "ID of the task that must complete before this task"
}
},
"required": [
"dependency_id"
]
}
}
}
},
"responses": {
"200": {
"description": "Updated task with new dependency",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Task"
}
}
}
},
"400": {
"description": "dependency_id missing or circular dependency detected"
},
"404": {
"description": "Task or dependency task not found"
}
}
}
GET /sprints
List sprints
Purple Planner sprints are the time-boxed iterations (planned | active | completed) of a project that tasks are assigned to via Task.sprintId, with velocity and completedPoints. Filter/sort server-side with $filter, $orderby, $select, $search, $top, $skip and $count (MCP clients send these as _filter, _orderby, …) rather than listing everything and filtering locally.
Authentication
[
{
"patAuth": []
}
]
Request, responses and operation details
{
"summary": "List sprints",
"description": "Purple Planner sprints are the time-boxed iterations (planned | active | completed) of a project that tasks are assigned to via Task.sprintId, with velocity and completedPoints. Filter/sort server-side with $filter, $orderby, $select, $search, $top, $skip and $count (MCP clients send these as _filter, _orderby, …) rather than listing everything and filtering locally.",
"parameters": [
{
"$ref": "#/components/parameters/InstanceId"
},
{
"in": "query",
"name": "limit",
"schema": {
"type": "integer",
"default": 50
},
"description": "Legacy page size (default 50). Prefer $top. Ignored when $top/$skip are used."
},
{
"in": "query",
"name": "cursor",
"schema": {
"type": "string"
},
"description": "Legacy keyset cursor — pass the previous response's nextCursor. Prefer $skip/$top."
},
{
"in": "query",
"name": "$filter",
"schema": {
"type": "string"
},
"description": "OData filter expression. Comparison operators: eq, ne, gt, ge, lt, le. Logical: and, or, not, with parentheses for grouping. String functions: contains(field,'x'), startswith(field,'x'), endswith(field,'x'). Membership: field in ('a','b'). Case-insensitive matching via tolower(field) / toupper(field). Nested fields use a slash, e.g. pto_balance/vacation_days. String literals use single quotes; dates are ISO-8601. Examples: \"status eq 'active'\"; \"salary gt 50000 and department eq 'Engineering'\"; \"contains(tolower(name),'acme')\"; \"closeDate gt 2026-01-01\"."
},
{
"in": "query",
"name": "$select",
"schema": {
"type": "string"
},
"description": "Comma-separated list of fields to return, e.g. \"id,firstName,email\". Nested fields via slash (\"pto_balance/vacation_days\"). The id field is always included."
},
{
"in": "query",
"name": "$orderby",
"schema": {
"type": "string"
},
"description": "Sort clause(s), comma-separated, each \"field [asc|desc]\" (asc is the default). Example: \"createdAt desc, lastName asc\". Null values sort last."
},
{
"in": "query",
"name": "$search",
"schema": {
"type": "string"
},
"description": "Free-text, case-insensitive substring match across the resource's searchable fields."
},
{
"in": "query",
"name": "$top",
"schema": {
"type": "integer"
},
"description": "Maximum number of items to return (page size)."
},
{
"in": "query",
"name": "$skip",
"schema": {
"type": "integer"
},
"description": "Number of matching items to skip before returning results (offset paging). Combine with $top."
},
{
"in": "query",
"name": "$count",
"schema": {
"type": "boolean"
},
"description": "When true, include @odata.count (total matches after $filter/$search, before paging) in the response."
}
],
"responses": {
"200": {
"description": "Paginated list",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"data": {
"type": "array",
"items": {
"$ref": "#/components/schemas/Sprint"
}
},
"nextCursor": {
"type": "string",
"nullable": true,
"description": "Cursor for the next page, or null on the last page. With $top/$skip this is the next $skip value; otherwise a keyset cursor."
},
"total": {
"type": "integer",
"description": "Total documents in the collection."
},
"@odata.count": {
"type": "integer",
"description": "Present only when $count=true: total matches after $filter/$search."
},
"@odata.truncated": {
"type": "boolean",
"description": "Present and true only when the collection exceeded the in-memory query cap and results may be incomplete."
}
}
}
}
}
},
"400": {
"description": "Invalid OData query (e.g. malformed $filter)"
}
}
}
POST /sprints
Create sprint
Plan a new time-boxed iteration on a Purple Planner project. New sprints are 'planned'; activate one with the start-sprint tool rather than by creating another.
Authentication
[
{
"patAuth": []
}
]
Request, responses and operation details
{
"summary": "Create sprint",
"description": "Plan a new time-boxed iteration on a Purple Planner project. New sprints are 'planned'; activate one with the start-sprint tool rather than by creating another.",
"parameters": [
{
"$ref": "#/components/parameters/InstanceId"
}
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Sprint"
}
}
}
},
"responses": {
"201": {
"description": "Created",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Sprint"
}
}
}
}
}
}
GET /sprints/{id}
Get sprint
Fetch one Purple Planner sprint by id with its dates, status, velocity, completedPoints and startedAt/completedAt stamps — the basis for burndown or sprint-health answers.
Authentication
[
{
"patAuth": []
}
]
Request, responses and operation details
{
"summary": "Get sprint",
"description": "Fetch one Purple Planner sprint by id with its dates, status, velocity, completedPoints and startedAt/completedAt stamps — the basis for burndown or sprint-health answers.",
"parameters": [
{
"$ref": "#/components/parameters/InstanceId"
},
{
"in": "path",
"name": "id",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "Found",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Sprint"
}
}
}
},
"404": {
"description": "Not found"
}
}
}
PATCH /sprints/{id}
Update sprint
Partially update sprint metadata such as name, dates or velocity. For lifecycle changes use start-sprint / complete-sprint instead — those enforce the rules and move the tasks.
Authentication
[
{
"patAuth": []
}
]
Request, responses and operation details
{
"summary": "Update sprint",
"description": "Partially update sprint metadata such as name, dates or velocity. For lifecycle changes use start-sprint / complete-sprint instead — those enforce the rules and move the tasks.",
"parameters": [
{
"$ref": "#/components/parameters/InstanceId"
},
{
"in": "path",
"name": "id",
"required": true,
"schema": {
"type": "string"
}
}
],
"requestBody": {
"description": "Fields to update (partial)",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Sprint"
}
}
}
},
"responses": {
"200": {
"description": "Updated",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Sprint"
}
}
}
},
"404": {
"description": "Not found"
}
}
}
DELETE /sprints/{id}
Delete sprint
Permanently delete a Purple Planner sprint. Destructive and irreversible, and tasks keep a dangling sprintId — to close out an iteration properly use complete-sprint, which rehomes unfinished work.
Authentication
[
{
"patAuth": []
}
]
Request, responses and operation details
{
"summary": "Delete sprint",
"description": "Permanently delete a Purple Planner sprint. Destructive and irreversible, and tasks keep a dangling sprintId — to close out an iteration properly use complete-sprint, which rehomes unfinished work.",
"parameters": [
{
"$ref": "#/components/parameters/InstanceId"
},
{
"in": "path",
"name": "id",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "Deleted",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"success": {
"type": "boolean"
}
}
}
}
}
},
"404": {
"description": "Not found"
}
}
}
POST /sprints/{id}/start
Start a sprint
Activate a planned Purple Planner sprint and stamp startedAt. Requires at least one task assigned to it (Task.sprintId), otherwise 422. Use this rather than patching status directly.
Authentication
[
{
"patAuth": []
}
]
Request, responses and operation details
{
"summary": "Start a sprint",
"description": "Activate a planned Purple Planner sprint and stamp startedAt. Requires at least one task assigned to it (Task.sprintId), otherwise 422. Use this rather than patching status directly.",
"parameters": [
{
"$ref": "#/components/parameters/InstanceId"
},
{
"in": "path",
"name": "id",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "Sprint started",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Sprint"
}
}
}
},
"404": {
"description": "Not found"
},
"422": {
"description": "Sprint has no tasks assigned and cannot be started"
}
}
}
POST /sprints/{id}/complete
Complete a sprint
Close out a Purple Planner sprint and sweep its unfinished tasks (status != done) into next_sprint_id, or back to the backlog (sprintId=null, status=todo) when that is omitted. Irreversible, and it rewrites those tasks — the response reports moved_tasks and warns when open work was moved.
Authentication
[
{
"patAuth": []
}
]
Request, responses and operation details
{
"summary": "Complete a sprint",
"description": "Close out a Purple Planner sprint and sweep its unfinished tasks (status != done) into next_sprint_id, or back to the backlog (sprintId=null, status=todo) when that is omitted. Irreversible, and it rewrites those tasks — the response reports moved_tasks and warns when open work was moved.",
"parameters": [
{
"$ref": "#/components/parameters/InstanceId"
},
{
"in": "path",
"name": "id",
"required": true,
"schema": {
"type": "string"
}
}
],
"requestBody": {
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"next_sprint_id": {
"type": "string",
"example": "SPR-002",
"description": "Sprint ID to move incomplete tasks into; omit to return tasks to backlog"
}
}
}
}
}
},
"responses": {
"200": {
"description": "Sprint completed",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"sprint": {
"$ref": "#/components/schemas/Sprint"
},
"moved_tasks": {
"type": "integer",
"example": 3,
"description": "Number of incomplete tasks moved or returned to backlog"
},
"warning": {
"type": "string",
"nullable": true,
"example": "Sprint completed with 3 open task(s) moved to backlog."
}
}
}
}
}
},
"404": {
"description": "Not found"
}
}
}
GET /milestones
List milestones
Purple Planner milestones are dated delivery checkpoints on a project (upcoming | in_progress | completed | at_risk), coarser than tasks and independent of sprints. Use these for "are we going to hit the date" questions. Filter/sort server-side with $filter, $orderby, $select, $search, $top, $skip and $count (MCP clients send these as _filter, _orderby, …) rather than listing everything and filtering locally.
Authentication
[
{
"patAuth": []
}
]
Request, responses and operation details
{
"summary": "List milestones",
"description": "Purple Planner milestones are dated delivery checkpoints on a project (upcoming | in_progress | completed | at_risk), coarser than tasks and independent of sprints. Use these for \"are we going to hit the date\" questions. Filter/sort server-side with $filter, $orderby, $select, $search, $top, $skip and $count (MCP clients send these as _filter, _orderby, …) rather than listing everything and filtering locally.",
"parameters": [
{
"$ref": "#/components/parameters/InstanceId"
},
{
"in": "query",
"name": "limit",
"schema": {
"type": "integer",
"default": 50
},
"description": "Legacy page size (default 50). Prefer $top. Ignored when $top/$skip are used."
},
{
"in": "query",
"name": "cursor",
"schema": {
"type": "string"
},
"description": "Legacy keyset cursor — pass the previous response's nextCursor. Prefer $skip/$top."
},
{
"in": "query",
"name": "$filter",
"schema": {
"type": "string"
},
"description": "OData filter expression. Comparison operators: eq, ne, gt, ge, lt, le. Logical: and, or, not, with parentheses for grouping. String functions: contains(field,'x'), startswith(field,'x'), endswith(field,'x'). Membership: field in ('a','b'). Case-insensitive matching via tolower(field) / toupper(field). Nested fields use a slash, e.g. pto_balance/vacation_days. String literals use single quotes; dates are ISO-8601. Examples: \"status eq 'active'\"; \"salary gt 50000 and department eq 'Engineering'\"; \"contains(tolower(name),'acme')\"; \"closeDate gt 2026-01-01\"."
},
{
"in": "query",
"name": "$select",
"schema": {
"type": "string"
},
"description": "Comma-separated list of fields to return, e.g. \"id,firstName,email\". Nested fields via slash (\"pto_balance/vacation_days\"). The id field is always included."
},
{
"in": "query",
"name": "$orderby",
"schema": {
"type": "string"
},
"description": "Sort clause(s), comma-separated, each \"field [asc|desc]\" (asc is the default). Example: \"createdAt desc, lastName asc\". Null values sort last."
},
{
"in": "query",
"name": "$search",
"schema": {
"type": "string"
},
"description": "Free-text, case-insensitive substring match across the resource's searchable fields."
},
{
"in": "query",
"name": "$top",
"schema": {
"type": "integer"
},
"description": "Maximum number of items to return (page size)."
},
{
"in": "query",
"name": "$skip",
"schema": {
"type": "integer"
},
"description": "Number of matching items to skip before returning results (offset paging). Combine with $top."
},
{
"in": "query",
"name": "$count",
"schema": {
"type": "boolean"
},
"description": "When true, include @odata.count (total matches after $filter/$search, before paging) in the response."
}
],
"responses": {
"200": {
"description": "Paginated list",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"data": {
"type": "array",
"items": {
"$ref": "#/components/schemas/Milestone"
}
},
"nextCursor": {
"type": "string",
"nullable": true,
"description": "Cursor for the next page, or null on the last page. With $top/$skip this is the next $skip value; otherwise a keyset cursor."
},
"total": {
"type": "integer",
"description": "Total documents in the collection."
},
"@odata.count": {
"type": "integer",
"description": "Present only when $count=true: total matches after $filter/$search."
},
"@odata.truncated": {
"type": "boolean",
"description": "Present and true only when the collection exceeded the in-memory query cap and results may be incomplete."
}
}
}
}
}
},
"400": {
"description": "Invalid OData query (e.g. malformed $filter)"
}
}
}
POST /milestones
Create milestone
Add a dated delivery checkpoint to a Purple Planner project. Use a milestone for a commitment date (e.g. 'MVP complete'), and a task for work someone has to do.
Authentication
[
{
"patAuth": []
}
]
Request, responses and operation details
{
"summary": "Create milestone",
"description": "Add a dated delivery checkpoint to a Purple Planner project. Use a milestone for a commitment date (e.g. 'MVP complete'), and a task for work someone has to do.",
"parameters": [
{
"$ref": "#/components/parameters/InstanceId"
}
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Milestone"
}
}
}
},
"responses": {
"201": {
"description": "Created",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Milestone"
}
}
}
}
}
}
GET /milestones/{id}
Get milestone
Fetch one Purple Planner milestone by id with its project, due date, status and completedAt stamp.
Authentication
[
{
"patAuth": []
}
]
Request, responses and operation details
{
"summary": "Get milestone",
"description": "Fetch one Purple Planner milestone by id with its project, due date, status and completedAt stamp.",
"parameters": [
{
"$ref": "#/components/parameters/InstanceId"
},
{
"in": "path",
"name": "id",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "Found",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Milestone"
}
}
}
},
"404": {
"description": "Not found"
}
}
}
PATCH /milestones/{id}
Update milestone
Partially update a Purple Planner milestone — move the due date, or flag it at_risk / in_progress / completed as delivery confidence changes.
Authentication
[
{
"patAuth": []
}
]
Request, responses and operation details
{
"summary": "Update milestone",
"description": "Partially update a Purple Planner milestone — move the due date, or flag it at_risk / in_progress / completed as delivery confidence changes.",
"parameters": [
{
"$ref": "#/components/parameters/InstanceId"
},
{
"in": "path",
"name": "id",
"required": true,
"schema": {
"type": "string"
}
}
],
"requestBody": {
"description": "Fields to update (partial)",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Milestone"
}
}
}
},
"responses": {
"200": {
"description": "Updated",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Milestone"
}
}
}
},
"404": {
"description": "Not found"
}
}
}
DELETE /milestones/{id}
Delete milestone
Permanently delete a Purple Planner milestone. Destructive and irreversible — if the checkpoint simply slipped or was met, update its dueDate or status instead.
Authentication
[
{
"patAuth": []
}
]
Request, responses and operation details
{
"summary": "Delete milestone",
"description": "Permanently delete a Purple Planner milestone. Destructive and irreversible — if the checkpoint simply slipped or was met, update its dueDate or status instead.",
"parameters": [
{
"$ref": "#/components/parameters/InstanceId"
},
{
"in": "path",
"name": "id",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "Deleted",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"success": {
"type": "boolean"
}
}
}
}
}
},
"404": {
"description": "Not found"
}
}
}
GET /initiatives
List initiatives
Purple Planner initiatives are the portfolio layer above projects (e.g. "26Q2 Platform Investments"), which projects roll up to via Project.initiativeIds. Use these for portfolio/quarterly-theme questions; the delivery work itself lives in projects and tasks. Filter/sort server-side with $filter, $orderby, $select, $search, $top, $skip and $count (MCP clients send these as _filter, _orderby, …) rather than listing everything and filtering locally.
Authentication
[
{
"patAuth": []
}
]
Request, responses and operation details
{
"summary": "List initiatives",
"description": "Purple Planner initiatives are the portfolio layer above projects (e.g. \"26Q2 Platform Investments\"), which projects roll up to via Project.initiativeIds. Use these for portfolio/quarterly-theme questions; the delivery work itself lives in projects and tasks. Filter/sort server-side with $filter, $orderby, $select, $search, $top, $skip and $count (MCP clients send these as _filter, _orderby, …) rather than listing everything and filtering locally.",
"parameters": [
{
"$ref": "#/components/parameters/InstanceId"
},
{
"in": "query",
"name": "limit",
"schema": {
"type": "integer",
"default": 50
},
"description": "Legacy page size (default 50). Prefer $top. Ignored when $top/$skip are used."
},
{
"in": "query",
"name": "cursor",
"schema": {
"type": "string"
},
"description": "Legacy keyset cursor — pass the previous response's nextCursor. Prefer $skip/$top."
},
{
"in": "query",
"name": "$filter",
"schema": {
"type": "string"
},
"description": "OData filter expression. Comparison operators: eq, ne, gt, ge, lt, le. Logical: and, or, not, with parentheses for grouping. String functions: contains(field,'x'), startswith(field,'x'), endswith(field,'x'). Membership: field in ('a','b'). Case-insensitive matching via tolower(field) / toupper(field). Nested fields use a slash, e.g. pto_balance/vacation_days. String literals use single quotes; dates are ISO-8601. Examples: \"status eq 'active'\"; \"salary gt 50000 and department eq 'Engineering'\"; \"contains(tolower(name),'acme')\"; \"closeDate gt 2026-01-01\"."
},
{
"in": "query",
"name": "$select",
"schema": {
"type": "string"
},
"description": "Comma-separated list of fields to return, e.g. \"id,firstName,email\". Nested fields via slash (\"pto_balance/vacation_days\"). The id field is always included."
},
{
"in": "query",
"name": "$orderby",
"schema": {
"type": "string"
},
"description": "Sort clause(s), comma-separated, each \"field [asc|desc]\" (asc is the default). Example: \"createdAt desc, lastName asc\". Null values sort last."
},
{
"in": "query",
"name": "$search",
"schema": {
"type": "string"
},
"description": "Free-text, case-insensitive substring match across the resource's searchable fields."
},
{
"in": "query",
"name": "$top",
"schema": {
"type": "integer"
},
"description": "Maximum number of items to return (page size)."
},
{
"in": "query",
"name": "$skip",
"schema": {
"type": "integer"
},
"description": "Number of matching items to skip before returning results (offset paging). Combine with $top."
},
{
"in": "query",
"name": "$count",
"schema": {
"type": "boolean"
},
"description": "When true, include @odata.count (total matches after $filter/$search, before paging) in the response."
}
],
"responses": {
"200": {
"description": "Paginated list",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"data": {
"type": "array",
"items": {
"$ref": "#/components/schemas/Initiative"
}
},
"nextCursor": {
"type": "string",
"nullable": true,
"description": "Cursor for the next page, or null on the last page. With $top/$skip this is the next $skip value; otherwise a keyset cursor."
},
"total": {
"type": "integer",
"description": "Total documents in the collection."
},
"@odata.count": {
"type": "integer",
"description": "Present only when $count=true: total matches after $filter/$search."
},
"@odata.truncated": {
"type": "boolean",
"description": "Present and true only when the collection exceeded the in-memory query cap and results may be incomplete."
}
}
}
}
}
},
"400": {
"description": "Invalid OData query (e.g. malformed $filter)"
}
}
}
POST /initiatives
Create initiative
Create a cross-project initiative in Purple Planner to group related projects under one portfolio theme and owner.
Authentication
[
{
"patAuth": []
}
]
Request, responses and operation details
{
"summary": "Create initiative",
"description": "Create a cross-project initiative in Purple Planner to group related projects under one portfolio theme and owner.",
"parameters": [
{
"$ref": "#/components/parameters/InstanceId"
}
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Initiative"
}
}
}
},
"responses": {
"201": {
"description": "Created",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Initiative"
}
}
}
}
}
}
GET /initiatives/{id}
Get initiative
Fetch one Purple Planner initiative by id (e.g. INIT-001) with its owner, status and target date. Find the projects underneath it by filtering projects on initiativeIds.
Authentication
[
{
"patAuth": []
}
]
Request, responses and operation details
{
"summary": "Get initiative",
"description": "Fetch one Purple Planner initiative by id (e.g. INIT-001) with its owner, status and target date. Find the projects underneath it by filtering projects on initiativeIds.",
"parameters": [
{
"$ref": "#/components/parameters/InstanceId"
},
{
"in": "path",
"name": "id",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "Found",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Initiative"
}
}
}
},
"404": {
"description": "Not found"
}
}
}
PATCH /initiatives/{id}
Update initiative
Partially update a Purple Planner initiative — rename it, change owner, target date or status (planned | active | completed).
Authentication
[
{
"patAuth": []
}
]
Request, responses and operation details
{
"summary": "Update initiative",
"description": "Partially update a Purple Planner initiative — rename it, change owner, target date or status (planned | active | completed).",
"parameters": [
{
"$ref": "#/components/parameters/InstanceId"
},
{
"in": "path",
"name": "id",
"required": true,
"schema": {
"type": "string"
}
}
],
"requestBody": {
"description": "Fields to update (partial)",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Initiative"
}
}
}
},
"responses": {
"200": {
"description": "Updated",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Initiative"
}
}
}
},
"404": {
"description": "Not found"
}
}
}
DELETE /initiatives/{id}
Delete initiative
Permanently delete a Purple Planner initiative. Destructive and irreversible, and projects keep a dangling id in initiativeIds — mark it completed instead when the portfolio theme simply ends.
Authentication
[
{
"patAuth": []
}
]
Request, responses and operation details
{
"summary": "Delete initiative",
"description": "Permanently delete a Purple Planner initiative. Destructive and irreversible, and projects keep a dangling id in initiativeIds — mark it completed instead when the portfolio theme simply ends.",
"parameters": [
{
"$ref": "#/components/parameters/InstanceId"
},
{
"in": "path",
"name": "id",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "Deleted",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"success": {
"type": "boolean"
}
}
}
}
}
},
"404": {
"description": "Not found"
}
}
}
GET /labels
List labels
Purple Planner labels are the tag vocabulary applied to tasks via Task.labelIds (e.g. "p1", "tech-debt"), each with a name, colour and description. Resolve a label name to its id here before filtering tasks by label. Filter/sort server-side with $filter, $orderby, $select, $search, $top, $skip and $count (MCP clients send these as _filter, _orderby, …) rather than listing everything and filtering locally.
Authentication
[
{
"patAuth": []
}
]
Request, responses and operation details
{
"summary": "List labels",
"description": "Purple Planner labels are the tag vocabulary applied to tasks via Task.labelIds (e.g. \"p1\", \"tech-debt\"), each with a name, colour and description. Resolve a label name to its id here before filtering tasks by label. Filter/sort server-side with $filter, $orderby, $select, $search, $top, $skip and $count (MCP clients send these as _filter, _orderby, …) rather than listing everything and filtering locally.",
"parameters": [
{
"$ref": "#/components/parameters/InstanceId"
},
{
"in": "query",
"name": "limit",
"schema": {
"type": "integer",
"default": 50
},
"description": "Legacy page size (default 50). Prefer $top. Ignored when $top/$skip are used."
},
{
"in": "query",
"name": "cursor",
"schema": {
"type": "string"
},
"description": "Legacy keyset cursor — pass the previous response's nextCursor. Prefer $skip/$top."
},
{
"in": "query",
"name": "$filter",
"schema": {
"type": "string"
},
"description": "OData filter expression. Comparison operators: eq, ne, gt, ge, lt, le. Logical: and, or, not, with parentheses for grouping. String functions: contains(field,'x'), startswith(field,'x'), endswith(field,'x'). Membership: field in ('a','b'). Case-insensitive matching via tolower(field) / toupper(field). Nested fields use a slash, e.g. pto_balance/vacation_days. String literals use single quotes; dates are ISO-8601. Examples: \"status eq 'active'\"; \"salary gt 50000 and department eq 'Engineering'\"; \"contains(tolower(name),'acme')\"; \"closeDate gt 2026-01-01\"."
},
{
"in": "query",
"name": "$select",
"schema": {
"type": "string"
},
"description": "Comma-separated list of fields to return, e.g. \"id,firstName,email\". Nested fields via slash (\"pto_balance/vacation_days\"). The id field is always included."
},
{
"in": "query",
"name": "$orderby",
"schema": {
"type": "string"
},
"description": "Sort clause(s), comma-separated, each \"field [asc|desc]\" (asc is the default). Example: \"createdAt desc, lastName asc\". Null values sort last."
},
{
"in": "query",
"name": "$search",
"schema": {
"type": "string"
},
"description": "Free-text, case-insensitive substring match across the resource's searchable fields."
},
{
"in": "query",
"name": "$top",
"schema": {
"type": "integer"
},
"description": "Maximum number of items to return (page size)."
},
{
"in": "query",
"name": "$skip",
"schema": {
"type": "integer"
},
"description": "Number of matching items to skip before returning results (offset paging). Combine with $top."
},
{
"in": "query",
"name": "$count",
"schema": {
"type": "boolean"
},
"description": "When true, include @odata.count (total matches after $filter/$search, before paging) in the response."
}
],
"responses": {
"200": {
"description": "Paginated list",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"data": {
"type": "array",
"items": {
"$ref": "#/components/schemas/Label"
}
},
"nextCursor": {
"type": "string",
"nullable": true,
"description": "Cursor for the next page, or null on the last page. With $top/$skip this is the next $skip value; otherwise a keyset cursor."
},
"total": {
"type": "integer",
"description": "Total documents in the collection."
},
"@odata.count": {
"type": "integer",
"description": "Present only when $count=true: total matches after $filter/$search."
},
"@odata.truncated": {
"type": "boolean",
"description": "Present and true only when the collection exceeded the in-memory query cap and results may be incomplete."
}
}
}
}
}
},
"400": {
"description": "Invalid OData query (e.g. malformed $filter)"
}
}
}
POST /labels
Create label
Add a new label to the Purple Planner tag vocabulary. Check the existing labels first — this creates a new tag rather than applying one; applying is done by setting labelIds on a task.
Authentication
[
{
"patAuth": []
}
]
Request, responses and operation details
{
"summary": "Create label",
"description": "Add a new label to the Purple Planner tag vocabulary. Check the existing labels first — this creates a new tag rather than applying one; applying is done by setting labelIds on a task.",
"parameters": [
{
"$ref": "#/components/parameters/InstanceId"
}
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Label"
}
}
}
},
"responses": {
"201": {
"description": "Created",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Label"
}
}
}
}
}
}
GET /labels/{id}
Get label
Fetch one Purple Planner label by id with its name, colour and description.
Authentication
[
{
"patAuth": []
}
]
Request, responses and operation details
{
"summary": "Get label",
"description": "Fetch one Purple Planner label by id with its name, colour and description.",
"parameters": [
{
"$ref": "#/components/parameters/InstanceId"
},
{
"in": "path",
"name": "id",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "Found",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Label"
}
}
}
},
"404": {
"description": "Not found"
}
}
}
PATCH /labels/{id}
Update label
Rename or recolour a Purple Planner label. Tasks referencing it by id pick the change up automatically.
Authentication
[
{
"patAuth": []
}
]
Request, responses and operation details
{
"summary": "Update label",
"description": "Rename or recolour a Purple Planner label. Tasks referencing it by id pick the change up automatically.",
"parameters": [
{
"$ref": "#/components/parameters/InstanceId"
},
{
"in": "path",
"name": "id",
"required": true,
"schema": {
"type": "string"
}
}
],
"requestBody": {
"description": "Fields to update (partial)",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Label"
}
}
}
},
"responses": {
"200": {
"description": "Updated",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Label"
}
}
}
},
"404": {
"description": "Not found"
}
}
}
DELETE /labels/{id}
Delete label
Permanently delete a Purple Planner label from the workspace vocabulary. Destructive — tasks keep the stale id in labelIds; to untag a single task, patch that task's labelIds instead.
Authentication
[
{
"patAuth": []
}
]
Request, responses and operation details
{
"summary": "Delete label",
"description": "Permanently delete a Purple Planner label from the workspace vocabulary. Destructive — tasks keep the stale id in labelIds; to untag a single task, patch that task's labelIds instead.",
"parameters": [
{
"$ref": "#/components/parameters/InstanceId"
},
{
"in": "path",
"name": "id",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "Deleted",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"success": {
"type": "boolean"
}
}
}
}
}
},
"404": {
"description": "Not found"
}
}
}
GET /users
List workspace members
Purple Planner workspace members are the people who can be a task assignee or project owner in this tool. This is not the chat roster, not the HR employee record, and not the canonical identity directory; use it to resolve an assignee or owner within Purple Planner. Filter/sort server-side with $filter, $orderby, $select, $search, $top, $skip and $count (MCP clients send these as _filter, _orderby, …) rather than listing everything and filtering locally.
Authentication
[
{
"patAuth": []
}
]
Request, responses and operation details
{
"summary": "List workspace members",
"description": "Purple Planner workspace members are the people who can be a task assignee or project owner in this tool. This is not the chat roster, not the HR employee record, and not the canonical identity directory; use it to resolve an assignee or owner within Purple Planner. Filter/sort server-side with $filter, $orderby, $select, $search, $top, $skip and $count (MCP clients send these as _filter, _orderby, …) rather than listing everything and filtering locally.",
"parameters": [
{
"$ref": "#/components/parameters/InstanceId"
},
{
"in": "query",
"name": "limit",
"schema": {
"type": "integer",
"default": 50
},
"description": "Legacy page size (default 50). Prefer $top. Ignored when $top/$skip are used."
},
{
"in": "query",
"name": "cursor",
"schema": {
"type": "string"
},
"description": "Legacy keyset cursor — pass the previous response's nextCursor. Prefer $skip/$top."
},
{
"in": "query",
"name": "$filter",
"schema": {
"type": "string"
},
"description": "OData filter expression. Comparison operators: eq, ne, gt, ge, lt, le. Logical: and, or, not, with parentheses for grouping. String functions: contains(field,'x'), startswith(field,'x'), endswith(field,'x'). Membership: field in ('a','b'). Case-insensitive matching via tolower(field) / toupper(field). Nested fields use a slash, e.g. pto_balance/vacation_days. String literals use single quotes; dates are ISO-8601. Examples: \"status eq 'active'\"; \"salary gt 50000 and department eq 'Engineering'\"; \"contains(tolower(name),'acme')\"; \"closeDate gt 2026-01-01\"."
},
{
"in": "query",
"name": "$select",
"schema": {
"type": "string"
},
"description": "Comma-separated list of fields to return, e.g. \"id,firstName,email\". Nested fields via slash (\"pto_balance/vacation_days\"). The id field is always included."
},
{
"in": "query",
"name": "$orderby",
"schema": {
"type": "string"
},
"description": "Sort clause(s), comma-separated, each \"field [asc|desc]\" (asc is the default). Example: \"createdAt desc, lastName asc\". Null values sort last."
},
{
"in": "query",
"name": "$search",
"schema": {
"type": "string"
},
"description": "Free-text, case-insensitive substring match across the resource's searchable fields."
},
{
"in": "query",
"name": "$top",
"schema": {
"type": "integer"
},
"description": "Maximum number of items to return (page size)."
},
{
"in": "query",
"name": "$skip",
"schema": {
"type": "integer"
},
"description": "Number of matching items to skip before returning results (offset paging). Combine with $top."
},
{
"in": "query",
"name": "$count",
"schema": {
"type": "boolean"
},
"description": "When true, include @odata.count (total matches after $filter/$search, before paging) in the response."
}
],
"responses": {
"200": {
"description": "Paginated list",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"data": {
"type": "array",
"items": {
"$ref": "#/components/schemas/User"
}
},
"nextCursor": {
"type": "string",
"nullable": true,
"description": "Cursor for the next page, or null on the last page. With $top/$skip this is the next $skip value; otherwise a keyset cursor."
},
"total": {
"type": "integer",
"description": "Total documents in the collection."
},
"@odata.count": {
"type": "integer",
"description": "Present only when $count=true: total matches after $filter/$search."
},
"@odata.truncated": {
"type": "boolean",
"description": "Present and true only when the collection exceeded the in-memory query cap and results may be incomplete."
}
}
}
}
}
},
"400": {
"description": "Invalid OData query (e.g. malformed $filter)"
}
}
}
POST /users
Add workspace member
Add a person to the Purple Planner workspace so they can be assigned issues or own projects. This creates a project-tool membership only — it does not create an HR employee record or an identity account.
Authentication
[
{
"patAuth": []
}
]
Request, responses and operation details
{
"summary": "Add workspace member",
"description": "Add a person to the Purple Planner workspace so they can be assigned issues or own projects. This creates a project-tool membership only — it does not create an HR employee record or an identity account.",
"parameters": [
{
"$ref": "#/components/parameters/InstanceId"
}
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/User"
}
}
}
},
"responses": {
"201": {
"description": "Created",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/User"
}
}
}
}
}
}
GET /users/{id}
Get workspace member
Fetch one Purple Planner workspace member by id (name, email, role) — the usual way to turn an assignee or owner reference into a person.
Authentication
[
{
"patAuth": []
}
]
Request, responses and operation details
{
"summary": "Get workspace member",
"description": "Fetch one Purple Planner workspace member by id (name, email, role) — the usual way to turn an assignee or owner reference into a person.",
"parameters": [
{
"$ref": "#/components/parameters/InstanceId"
},
{
"in": "path",
"name": "id",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "Found",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/User"
}
}
}
},
"404": {
"description": "Not found"
}
}
}
PATCH /users/{id}
Update workspace member
Update a Purple Planner member's name, email or role. Affects this project tool only, not their HR record or chat account.
Authentication
[
{
"patAuth": []
}
]
Request, responses and operation details
{
"summary": "Update workspace member",
"description": "Update a Purple Planner member's name, email or role. Affects this project tool only, not their HR record or chat account.",
"parameters": [
{
"$ref": "#/components/parameters/InstanceId"
},
{
"in": "path",
"name": "id",
"required": true,
"schema": {
"type": "string"
}
}
],
"requestBody": {
"description": "Fields to update (partial)",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/User"
}
}
}
},
"responses": {
"200": {
"description": "Updated",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/User"
}
}
}
},
"404": {
"description": "Not found"
}
}
}
DELETE /users/{id}
Remove workspace member
Remove a member from the Purple Planner workspace. Destructive and irreversible; their tasks and projects keep the id as a dangling assignee/owner, so reassign that work first.
Authentication
[
{
"patAuth": []
}
]
Request, responses and operation details
{
"summary": "Remove workspace member",
"description": "Remove a member from the Purple Planner workspace. Destructive and irreversible; their tasks and projects keep the id as a dangling assignee/owner, so reassign that work first.",
"parameters": [
{
"$ref": "#/components/parameters/InstanceId"
},
{
"in": "path",
"name": "id",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "Deleted",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"success": {
"type": "boolean"
}
}
}
}
}
},
"404": {
"description": "Not found"
}
}
}
GET /status_updates
List status updates
Purple Planner status updates are narrative progress posts written against a project or initiative, each with a health flag (on_track | at_risk | off_track). Use these for "what's the latest on X" questions; numeric progress lives on the project record. Filter/sort server-side with $filter, $orderby, $select, $search, $top, $skip and $count (MCP clients send these as _filter, _orderby, …) rather than listing everything and filtering locally.
Authentication
[
{
"patAuth": []
}
]
Request, responses and operation details
{
"summary": "List status updates",
"description": "Purple Planner status updates are narrative progress posts written against a project or initiative, each with a health flag (on_track | at_risk | off_track). Use these for \"what's the latest on X\" questions; numeric progress lives on the project record. Filter/sort server-side with $filter, $orderby, $select, $search, $top, $skip and $count (MCP clients send these as _filter, _orderby, …) rather than listing everything and filtering locally.",
"parameters": [
{
"$ref": "#/components/parameters/InstanceId"
},
{
"in": "query",
"name": "limit",
"schema": {
"type": "integer",
"default": 50
},
"description": "Legacy page size (default 50). Prefer $top. Ignored when $top/$skip are used."
},
{
"in": "query",
"name": "cursor",
"schema": {
"type": "string"
},
"description": "Legacy keyset cursor — pass the previous response's nextCursor. Prefer $skip/$top."
},
{
"in": "query",
"name": "$filter",
"schema": {
"type": "string"
},
"description": "OData filter expression. Comparison operators: eq, ne, gt, ge, lt, le. Logical: and, or, not, with parentheses for grouping. String functions: contains(field,'x'), startswith(field,'x'), endswith(field,'x'). Membership: field in ('a','b'). Case-insensitive matching via tolower(field) / toupper(field). Nested fields use a slash, e.g. pto_balance/vacation_days. String literals use single quotes; dates are ISO-8601. Examples: \"status eq 'active'\"; \"salary gt 50000 and department eq 'Engineering'\"; \"contains(tolower(name),'acme')\"; \"closeDate gt 2026-01-01\"."
},
{
"in": "query",
"name": "$select",
"schema": {
"type": "string"
},
"description": "Comma-separated list of fields to return, e.g. \"id,firstName,email\". Nested fields via slash (\"pto_balance/vacation_days\"). The id field is always included."
},
{
"in": "query",
"name": "$orderby",
"schema": {
"type": "string"
},
"description": "Sort clause(s), comma-separated, each \"field [asc|desc]\" (asc is the default). Example: \"createdAt desc, lastName asc\". Null values sort last."
},
{
"in": "query",
"name": "$search",
"schema": {
"type": "string"
},
"description": "Free-text, case-insensitive substring match across the resource's searchable fields."
},
{
"in": "query",
"name": "$top",
"schema": {
"type": "integer"
},
"description": "Maximum number of items to return (page size)."
},
{
"in": "query",
"name": "$skip",
"schema": {
"type": "integer"
},
"description": "Number of matching items to skip before returning results (offset paging). Combine with $top."
},
{
"in": "query",
"name": "$count",
"schema": {
"type": "boolean"
},
"description": "When true, include @odata.count (total matches after $filter/$search, before paging) in the response."
}
],
"responses": {
"200": {
"description": "Paginated list",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"data": {
"type": "array",
"items": {
"$ref": "#/components/schemas/StatusUpdate"
}
},
"nextCursor": {
"type": "string",
"nullable": true,
"description": "Cursor for the next page, or null on the last page. With $top/$skip this is the next $skip value; otherwise a keyset cursor."
},
"total": {
"type": "integer",
"description": "Total documents in the collection."
},
"@odata.count": {
"type": "integer",
"description": "Present only when $count=true: total matches after $filter/$search."
},
"@odata.truncated": {
"type": "boolean",
"description": "Present and true only when the collection exceeded the in-memory query cap and results may be incomplete."
}
}
}
}
}
},
"400": {
"description": "Invalid OData query (e.g. malformed $filter)"
}
}
}
POST /status_updates
Post status update
Post a narrative progress update against a Purple Planner project or initiative, with body text and a health flag. Use this to communicate status; use update-project to change the project's own status field.
Authentication
[
{
"patAuth": []
}
]
Request, responses and operation details
{
"summary": "Post status update",
"description": "Post a narrative progress update against a Purple Planner project or initiative, with body text and a health flag. Use this to communicate status; use update-project to change the project's own status field.",
"parameters": [
{
"$ref": "#/components/parameters/InstanceId"
}
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/StatusUpdate"
}
}
}
},
"responses": {
"201": {
"description": "Created",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/StatusUpdate"
}
}
}
}
}
}
GET /status_updates/{id}
Get status update
Fetch one Purple Planner status update by id with its author, body, health flag and the project or initiative it covers.
Authentication
[
{
"patAuth": []
}
]
Request, responses and operation details
{
"summary": "Get status update",
"description": "Fetch one Purple Planner status update by id with its author, body, health flag and the project or initiative it covers.",
"parameters": [
{
"$ref": "#/components/parameters/InstanceId"
},
{
"in": "path",
"name": "id",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "Found",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/StatusUpdate"
}
}
}
},
"404": {
"description": "Not found"
}
}
}
PATCH /status_updates/{id}
Update status update
Edit a previously posted Purple Planner status update — correct the body text or revise the health flag.
Authentication
[
{
"patAuth": []
}
]
Request, responses and operation details
{
"summary": "Update status update",
"description": "Edit a previously posted Purple Planner status update — correct the body text or revise the health flag.",
"parameters": [
{
"$ref": "#/components/parameters/InstanceId"
},
{
"in": "path",
"name": "id",
"required": true,
"schema": {
"type": "string"
}
}
],
"requestBody": {
"description": "Fields to update (partial)",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/StatusUpdate"
}
}
}
},
"responses": {
"200": {
"description": "Updated",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/StatusUpdate"
}
}
}
},
"404": {
"description": "Not found"
}
}
}
DELETE /status_updates/{id}
Delete status update
Permanently delete a Purple Planner status update, removing it from the project's update history. Destructive and irreversible.
Authentication
[
{
"patAuth": []
}
]
Request, responses and operation details
{
"summary": "Delete status update",
"description": "Permanently delete a Purple Planner status update, removing it from the project's update history. Destructive and irreversible.",
"parameters": [
{
"$ref": "#/components/parameters/InstanceId"
},
{
"in": "path",
"name": "id",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "Deleted",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"success": {
"type": "boolean"
}
}
}
}
}
},
"404": {
"description": "Not found"
}
}
}
GET /teams
List teams
Purple Planner teams are the delivery squads that own projects (Project.teamIds) and issues (Task.teamId), each with a short key such as PLAT and a memberIds roster. These are product/engineering squads, not HR departments or chat channels. Filter/sort server-side with $filter, $orderby, $select, $search, $top, $skip and $count (MCP clients send these as _filter, _orderby, …) rather than listing everything and filtering locally.
Authentication
[
{
"patAuth": []
}
]
Request, responses and operation details
{
"summary": "List teams",
"description": "Purple Planner teams are the delivery squads that own projects (Project.teamIds) and issues (Task.teamId), each with a short key such as PLAT and a memberIds roster. These are product/engineering squads, not HR departments or chat channels. Filter/sort server-side with $filter, $orderby, $select, $search, $top, $skip and $count (MCP clients send these as _filter, _orderby, …) rather than listing everything and filtering locally.",
"parameters": [
{
"$ref": "#/components/parameters/InstanceId"
},
{
"in": "query",
"name": "limit",
"schema": {
"type": "integer",
"default": 50
},
"description": "Legacy page size (default 50). Prefer $top. Ignored when $top/$skip are used."
},
{
"in": "query",
"name": "cursor",
"schema": {
"type": "string"
},
"description": "Legacy keyset cursor — pass the previous response's nextCursor. Prefer $skip/$top."
},
{
"in": "query",
"name": "$filter",
"schema": {
"type": "string"
},
"description": "OData filter expression. Comparison operators: eq, ne, gt, ge, lt, le. Logical: and, or, not, with parentheses for grouping. String functions: contains(field,'x'), startswith(field,'x'), endswith(field,'x'). Membership: field in ('a','b'). Case-insensitive matching via tolower(field) / toupper(field). Nested fields use a slash, e.g. pto_balance/vacation_days. String literals use single quotes; dates are ISO-8601. Examples: \"status eq 'active'\"; \"salary gt 50000 and department eq 'Engineering'\"; \"contains(tolower(name),'acme')\"; \"closeDate gt 2026-01-01\"."
},
{
"in": "query",
"name": "$select",
"schema": {
"type": "string"
},
"description": "Comma-separated list of fields to return, e.g. \"id,firstName,email\". Nested fields via slash (\"pto_balance/vacation_days\"). The id field is always included."
},
{
"in": "query",
"name": "$orderby",
"schema": {
"type": "string"
},
"description": "Sort clause(s), comma-separated, each \"field [asc|desc]\" (asc is the default). Example: \"createdAt desc, lastName asc\". Null values sort last."
},
{
"in": "query",
"name": "$search",
"schema": {
"type": "string"
},
"description": "Free-text, case-insensitive substring match across the resource's searchable fields."
},
{
"in": "query",
"name": "$top",
"schema": {
"type": "integer"
},
"description": "Maximum number of items to return (page size)."
},
{
"in": "query",
"name": "$skip",
"schema": {
"type": "integer"
},
"description": "Number of matching items to skip before returning results (offset paging). Combine with $top."
},
{
"in": "query",
"name": "$count",
"schema": {
"type": "boolean"
},
"description": "When true, include @odata.count (total matches after $filter/$search, before paging) in the response."
}
],
"responses": {
"200": {
"description": "Paginated list",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"data": {
"type": "array",
"items": {
"$ref": "#/components/schemas/Team"
}
},
"nextCursor": {
"type": "string",
"nullable": true,
"description": "Cursor for the next page, or null on the last page. With $top/$skip this is the next $skip value; otherwise a keyset cursor."
},
"total": {
"type": "integer",
"description": "Total documents in the collection."
},
"@odata.count": {
"type": "integer",
"description": "Present only when $count=true: total matches after $filter/$search."
},
"@odata.truncated": {
"type": "boolean",
"description": "Present and true only when the collection exceeded the in-memory query cap and results may be incomplete."
}
}
}
}
}
},
"400": {
"description": "Invalid OData query (e.g. malformed $filter)"
}
}
}
POST /teams
Create team
Create a delivery squad in Purple Planner with a short key (e.g. PLAT) and member roster, so projects and issues can be assigned to it.
Authentication
[
{
"patAuth": []
}
]
Request, responses and operation details
{
"summary": "Create team",
"description": "Create a delivery squad in Purple Planner with a short key (e.g. PLAT) and member roster, so projects and issues can be assigned to it.",
"parameters": [
{
"$ref": "#/components/parameters/InstanceId"
}
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Team"
}
}
}
},
"responses": {
"201": {
"description": "Created",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Team"
}
}
}
}
}
}
GET /teams/{id}
Get team
Fetch one Purple Planner team by id with its name, key and memberIds roster.
Authentication
[
{
"patAuth": []
}
]
Request, responses and operation details
{
"summary": "Get team",
"description": "Fetch one Purple Planner team by id with its name, key and memberIds roster.",
"parameters": [
{
"$ref": "#/components/parameters/InstanceId"
},
{
"in": "path",
"name": "id",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "Found",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Team"
}
}
}
},
"404": {
"description": "Not found"
}
}
}
PATCH /teams/{id}
Update team
Rename a Purple Planner team, change its key, or add/remove people by rewriting memberIds.
Authentication
[
{
"patAuth": []
}
]
Request, responses and operation details
{
"summary": "Update team",
"description": "Rename a Purple Planner team, change its key, or add/remove people by rewriting memberIds.",
"parameters": [
{
"$ref": "#/components/parameters/InstanceId"
},
{
"in": "path",
"name": "id",
"required": true,
"schema": {
"type": "string"
}
}
],
"requestBody": {
"description": "Fields to update (partial)",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Team"
}
}
}
},
"responses": {
"200": {
"description": "Updated",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Team"
}
}
}
},
"404": {
"description": "Not found"
}
}
}
DELETE /teams/{id}
Delete team
Permanently delete a Purple Planner team. Destructive and irreversible, and projects/tasks keep a dangling team reference — reassign their ownership first.
Authentication
[
{
"patAuth": []
}
]
Request, responses and operation details
{
"summary": "Delete team",
"description": "Permanently delete a Purple Planner team. Destructive and irreversible, and projects/tasks keep a dangling team reference — reassign their ownership first.",
"parameters": [
{
"$ref": "#/components/parameters/InstanceId"
},
{
"in": "path",
"name": "id",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "Deleted",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"success": {
"type": "boolean"
}
}
}
}
}
},
"404": {
"description": "Not found"
}
}
}