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.
---
**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`.
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.
---
**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 /todos/config
Get the tenant's Todos tabs and filter facets
Mirrors GET /api/sn_ex_emp_hub_mw/todos/config. No query parameters.
Authentication
[
{
"patAuth": []
}
]
Request, responses and operation details
{
"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"
}
}
}
}
}
}
GET /todos
List the caller's todos (tasks + approvals, unified)
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.
Authentication
[
{
"patAuth": []
}
]
Request, responses and operation details
{
"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"
}
}
}
GET /tasks
List tasks
Authentication
[
{
"patAuth": []
}
]
Request, responses and operation details
{
"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 /tasks
Create task
Authentication
[
{
"patAuth": []
}
]
Request, responses and operation details
{
"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"
}
}
}
}
}
}
GET /tasks/{id}
Get task
Authentication
[
{
"patAuth": []
}
]
Request, responses and operation details
{
"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 /tasks/{id}
Update task
Authentication
[
{
"patAuth": []
}
]
Request, responses and operation details
{
"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 /tasks/{id}
Delete task
Authentication
[
{
"patAuth": []
}
]
Request, responses and operation details
{
"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"
}
}
}
GET /approvals
List approvals
Authentication
[
{
"patAuth": []
}
]
Request, responses and operation details
{
"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 /approvals
Create approval
Authentication
[
{
"patAuth": []
}
]
Request, responses and operation details
{
"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"
}
}
}
}
}
}
GET /approvals/{id}
Get approval
Authentication
[
{
"patAuth": []
}
]
Request, responses and operation details
{
"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 /approvals/{id}
Update approval
Authentication
[
{
"patAuth": []
}
]
Request, responses and operation details
{
"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 /approvals/{id}
Delete approval
Authentication
[
{
"patAuth": []
}
]
Request, responses and operation details
{
"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"
}
}
}
POST /approvals/{id}/approve
Approve an approval
Authentication
[
{
"patAuth": []
}
]
Request, responses and operation details
{
"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"
}
}
}
POST /approvals/{id}/reject
Reject an approval
Authentication
[
{
"patAuth": []
}
]
Request, responses and operation details
{
"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"
}
}
}