{"openapi":"3.0.0","info":{"title":"Purple Suite Tasks & Approvals API","version":"1.0.0","description":"Internal admin CRUD (/tasks, /approvals) is OData-queryable per the platform standard. GET /todos and GET /todos/config instead mirror the REAL sn_ex_emp_hub_mw Todos API response shape byte-for-byte (see mw-task-mock/Specs/schemas/mw_todos*.schema.json) — that is the surface an agent connector should call.\n\n---\n\n**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`."},"servers":[{"url":"https://marketplace.moveworks.com/api/purple-suite/tasks-approvals"}],"security":[{"patAuth":[]}],"components":{"securitySchemes":{"patAuth":{"type":"http","scheme":"bearer"}},"parameters":{"InstanceId":{"in":"header","name":"X-Instance-ID","required":true,"schema":{"type":"string"}},"ImpersonateUser":{"in":"header","name":"X-PS-Impersonate-User","required":false,"schema":{"type":"string","format":"email"},"description":"Optional. Act as a specific user in the target instance — the value is that user's email (must exist in the instance). Resolution order for the effective user: (1) this header, if set; (2) the instance's configured default MCP user; (3) otherwise no user is bound. User-scoped paths (e.g. /users/{email}/…) also accept the literal \"me\", and Graph-style apps serve /me/… twins of those paths (Graph: /me ≡ /users/{signed-in-user}) — both resolve to whatever this header (or the default MCP user) points at. An explicit path email always wins over the header. The unified MCP server does not send this header — it relies on the per-instance default — so it is primarily for direct REST/HTTP callers."}},"schemas":{"Task":{"type":"object","properties":{"id":{"type":"string","readOnly":true,"example":"TASK-0001"},"title":{"type":"string","example":"Complete benefits enrollment"},"description":{"type":"string","example":"Review and confirm your benefits elections for this plan year."},"assignee_email":{"type":"string","format":"email","example":"abraham.lincoln@acme.com"},"delegated_to_email":{"type":"string","format":"email","nullable":true,"example":null},"task_type_key":{"type":"string","enum":["hr_tasks","universal_task","document_tasks","legal_hold_tasks","learning_tasks","time_off_request_todos","leave_of_absence_request_todos","termination_request_todos","job_change_request_todos","compensation_change_request_todos","timesheet_request_todos","spend_authorization_request_todos","invoice","milestone","receipt","sourcing","mentorship_requests","post_feedback","job_requisition_request_todos","correct_time_off_request_todos","hr_acceptance","journey_accelerator_to_dos","cd_task","supplier_task","actions","procurement_tasks"],"example":"hr_tasks"},"sub_type_name":{"type":"string","example":""},"status":{"type":"string","enum":["pending","completed","cancelled"],"example":"pending"},"priority":{"type":"string","enum":["critical","high","medium","low"],"nullable":true,"example":"high"},"due_date":{"type":"string","format":"date-time","nullable":true,"example":"2026-07-22T00:00:00.000Z"},"requester_name":{"type":"string","example":""},"createdAt":{"type":"string","format":"date-time","readOnly":true},"updatedAt":{"type":"string","format":"date-time","readOnly":true}}},"Approval":{"type":"object","properties":{"id":{"type":"string","readOnly":true,"example":"APR-0001"},"title":{"type":"string","example":"Contract review — vendor MSA"},"requester_name":{"type":"string","example":"Jordan Reyes"},"approver_email":{"type":"string","format":"email","example":"jordan.reyes@acme.com"},"delegated_to_email":{"type":"string","format":"email","nullable":true,"example":null},"task_type_key":{"type":"string","enum":["approvals","concur_expense_report_approval"],"example":"approvals"},"sub_type_name":{"type":"string","example":"Legal Matter"},"status":{"type":"string","enum":["pending","approved","rejected"],"example":"pending"},"priority":{"type":"string","enum":["critical","high","medium","low"],"nullable":true,"example":"high"},"due_date":{"type":"string","format":"date-time","nullable":true},"received_date":{"type":"string","format":"date","example":"2026-07-15"},"summary":{"type":"string","example":"$18,400 vendor procurement request"},"approval_decision":{"type":"object","readOnly":true,"nullable":true,"properties":{"decided_by":{"type":"string"},"decided_at":{"type":"string","format":"date-time"},"reason":{"type":"string"}}},"createdAt":{"type":"string","format":"date-time","readOnly":true},"updatedAt":{"type":"string","format":"date-time","readOnly":true}}},"TodosConfigFilterItem":{"type":"object","properties":{"filterName":{"type":"string"},"filterId":{"type":"string"},"isGroup":{"type":"boolean","description":"True when this entry is a group whose children are in subFilters."},"filterDescription":{"type":"string"},"subFilters":{"type":"array","items":{"$ref":"#/components/schemas/TodosConfigFilterItem"}}}},"TodosConfigResult":{"type":"object","properties":{"result":{"type":"object","properties":{"tabs":{"type":"array","items":{"type":"object","properties":{"name":{"type":"string"},"label":{"type":"string"}}}},"filters":{"type":"array","items":{"type":"object","properties":{"title":{"type":"string"},"categoryId":{"type":"string"},"filters":{"type":"array","items":{"$ref":"#/components/schemas/TodosConfigFilterItem"}}}}}}}}},"TodosResult":{"type":"object","properties":{"result":{"type":"object","properties":{"todos":{"type":"array","items":{"type":"object"}},"todoSysIds":{"type":"array","items":{"type":"string"}},"count":{"type":"integer"}}}}}}},"paths":{"/todos/config":{"get":{"summary":"Get the tenant's Todos tabs and filter facets","description":"Mirrors GET /api/sn_ex_emp_hub_mw/todos/config. No query parameters.","parameters":[{"$ref":"#/components/parameters/InstanceId"}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TodosConfigResult"}}}}}}},"/todos":{"get":{"summary":"List the caller's todos (tasks + approvals, unified)","description":"Mirrors GET /api/sn_ex_emp_hub_mw/todos?tabs=<tab>. The effective caller resolves via X-PS-Impersonate-User / the instance's default MCP user.","parameters":[{"$ref":"#/components/parameters/InstanceId"},{"in":"query","name":"tabs","schema":{"type":"string","enum":["open","completed"],"default":"open"},"description":"Which tab to list."},{"in":"query","name":"filters","schema":{"type":"string"},"description":"One or more filterId values from /todos/config (comma-separated or repeated). ANDed across categories, ORed within a category."},{"in":"query","name":"limit","schema":{"type":"integer"},"description":"Max todos to return."},{"in":"query","name":"excludeSysIds","schema":{"type":"string"},"description":"Comma-separated ids to exclude (already seen in a prior page/turn)."}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TodosResult"}}}},"400":{"description":"Cannot resolve the effective caller"}}}},"/tasks":{"get":{"summary":"List tasks","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."}}}}}},"400":{"description":"Invalid OData query (e.g. malformed $filter)"}}},"post":{"summary":"Create task","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"}}}}}}},"/tasks/{id}":{"get":{"summary":"Get task","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":{"summary":"Update task","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"}}},"delete":{"summary":"Delete task","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"}}}},"/approvals":{"get":{"summary":"List approvals","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/Approval"}},"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":{"summary":"Create approval","parameters":[{"$ref":"#/components/parameters/InstanceId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Approval"}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Approval"}}}}}}},"/approvals/{id}":{"get":{"summary":"Get approval","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/Approval"}}}},"404":{"description":"Not found"}}},"patch":{"summary":"Update approval","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/Approval"}}}},"responses":{"200":{"description":"Updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Approval"}}}},"404":{"description":"Not found"}}},"delete":{"summary":"Delete approval","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"}}}},"/approvals/{id}/approve":{"post":{"summary":"Approve an approval","parameters":[{"$ref":"#/components/parameters/InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["decided_by"],"properties":{"decided_by":{"type":"string","example":"abraham.lincoln@acme.com"},"reason":{"type":"string"}}}}}},"responses":{"200":{"description":"Updated approval","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Approval"}}}},"404":{"description":"Not found"},"422":{"description":"Already decided"}}}},"/approvals/{id}/reject":{"post":{"summary":"Reject an approval","parameters":[{"$ref":"#/components/parameters/InstanceId"},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["decided_by","reason"],"properties":{"decided_by":{"type":"string"},"reason":{"type":"string"}}}}}},"responses":{"200":{"description":"Updated approval","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Approval"}}}},"404":{"description":"Not found"},"422":{"description":"Already decided"}}}}}}