Purple Identity — the canonical people directory for a Purple Suite instance. Every other app (HRIS, Calendar, Expense, ITSM, …) draws its internal people from this same roster, so an email means the same person everywhere. Exposes an OData-queryable /people collection AND an emulation of the Moveworks Identity Gateway (GET /users, GET /users/{userId}) so a deployment can sync its user directory directly from Purple Suite.
---
**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`.
Purple Identity — the canonical people directory for a Purple Suite instance. Every other app (HRIS, Calendar, Expense, ITSM, …) draws its internal people from this same roster, so an email means the same person everywhere. Exposes an OData-queryable /people collection AND an emulation of the Moveworks Identity Gateway (GET /users, GET /users/{userId}) so a deployment can sync its user directory directly from Purple Suite.
---
**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 /people
List people in the Purple Identity directory
The canonical internal roster for this Purple Suite instance — every other app (HRIS, ITSM, Expense, Calendar, PM, …) draws its employees, owners and assignees from these same records, so an email means the same human everywhere. Returns flat Person records (email, first/last/full name, title, department, managerEmail, country, city, timezone, startDate, workStatus). Supports server-side OData querying — $filter, $search, $orderby, $select, $top, $skip, $count (MCP clients see these as _filter, _search, _orderby, _select, _top, _skip, _count because $ is not a legal tool-input key). Example: $filter=department eq 'Engineering' and country eq 'US'. Use this to resolve a person, look up a manager, or build an org view; use identity__list__users only when you need the same people in the Moveworks Identity Gateway wire shape.
Authentication
[
{
"patAuth": []
}
]
Request, responses and operation details
{
"summary": "List people in the Purple Identity directory",
"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/Person"
}
},
"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)"
}
},
"description": "The canonical internal roster for this Purple Suite instance — every other app (HRIS, ITSM, Expense, Calendar, PM, …) draws its employees, owners and assignees from these same records, so an email means the same human everywhere. Returns flat Person records (email, first/last/full name, title, department, managerEmail, country, city, timezone, startDate, workStatus). Supports server-side OData querying — $filter, $search, $orderby, $select, $top, $skip, $count (MCP clients see these as _filter, _search, _orderby, _select, _top, _skip, _count because $ is not a legal tool-input key). Example: $filter=department eq 'Engineering' and country eq 'US'. Use this to resolve a person, look up a manager, or build an org view; use identity__list__users only when you need the same people in the Moveworks Identity Gateway wire shape."
}
POST /people
Add a person to the Purple Identity directory
Creates a Person in the instance's canonical internal roster, making that email resolvable across every other Purple app. Use only for internal staff (employees, contingent workers, interns) — external parties such as CRM contacts, ATS candidates or bank customers live in their own app and must not be added here.
Authentication
[
{
"patAuth": []
}
]
Request, responses and operation details
{
"summary": "Add a person to the Purple Identity directory",
"parameters": [
{
"$ref": "#/components/parameters/InstanceId"
}
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Person"
}
}
}
},
"responses": {
"201": {
"description": "Created",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Person"
}
}
}
}
},
"description": "Creates a Person in the instance's canonical internal roster, making that email resolvable across every other Purple app. Use only for internal staff (employees, contingent workers, interns) — external parties such as CRM contacts, ATS candidates or bank customers live in their own app and must not be added here."
}
GET /people/{id}
Get one person from the Purple Identity directory
Fetches a single Person by directory id (e.g. PERSON-0001) from the canonical internal roster. If you only have an email, call identity__list__people with $filter=email eq 'someone@acme.com' instead. Returns the flat Person shape; identity__get__users returns the same human in the nested Identity Gateway shape.
Authentication
[
{
"patAuth": []
}
]
Request, responses and operation details
{
"summary": "Get one person from the Purple Identity directory",
"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/Person"
}
}
}
},
"404": {
"description": "Not found"
}
},
"description": "Fetches a single Person by directory id (e.g. PERSON-0001) from the canonical internal roster. If you only have an email, call identity__list__people with $filter=email eq 'someone@acme.com' instead. Returns the flat Person shape; identity__get__users returns the same human in the nested Identity Gateway shape."
}
PATCH /people/{id}
Update a person's directory record
Partially updates a Person in the canonical roster (e.g. title, department, managerEmail, location, workStatus). Because every other Purple app reads people from here, the change is immediately visible suite-wide. Send only the fields you want to change.
Authentication
[
{
"patAuth": []
}
]
Request, responses and operation details
{
"summary": "Update a person's directory record",
"description": "Partially updates a Person in the canonical roster (e.g. title, department, managerEmail, location, workStatus). Because every other Purple app reads people from here, the change is immediately visible suite-wide. Send only the fields you want to change.",
"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/Person"
}
}
}
},
"responses": {
"200": {
"description": "Updated",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Person"
}
}
}
},
"404": {
"description": "Not found"
}
}
}
DELETE /people/{id}
Delete a person from the Purple Identity directory
DESTRUCTIVE and irreversible: permanently removes the Person from the canonical roster, so their email no longer resolves in any Purple app and existing references to them (tickets, expenses, enrollments) are left dangling. For a departure, prefer identity__update__people to change their status rather than deleting the record.
Authentication
[
{
"patAuth": []
}
]
Request, responses and operation details
{
"summary": "Delete a person from the Purple Identity directory",
"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"
}
},
"description": "DESTRUCTIVE and irreversible: permanently removes the Person from the canonical roster, so their email no longer resolves in any Purple app and existing references to them (tickets, expenses, enrollments) are left dangling. For a departure, prefer identity__update__people to change their status rather than deleting the record."
}
GET /users
List users in the Moveworks Identity Gateway shape
Emulates the Moveworks Identity Gateway 'List Users' contract for user-directory sync. These are the SAME humans as identity__list__people, projected into the gateway's nested envelope ({ results: [{ user, system_identity, last_updated_at }], next_page_token }) — reach for it when you are exercising or debugging a Moveworks identity sync, and for everything else use identity__list__people, which returns the flat, easier-to-read Person records. This endpoint is NOT OData: page with pageToken/pageSize and narrow results with the gateway's own `filter` expression (e.g. filter=department eq 'Engineering'), not $filter/$top.
Authentication
[
{
"patAuth": []
}
]
Request, responses and operation details
{
"x-odata": false,
"summary": "List users in the Moveworks Identity Gateway shape",
"description": "Emulates the Moveworks Identity Gateway 'List Users' contract for user-directory sync. These are the SAME humans as identity__list__people, projected into the gateway's nested envelope ({ results: [{ user, system_identity, last_updated_at }], next_page_token }) — reach for it when you are exercising or debugging a Moveworks identity sync, and for everything else use identity__list__people, which returns the flat, easier-to-read Person records. This endpoint is NOT OData: page with pageToken/pageSize and narrow results with the gateway's own `filter` expression (e.g. filter=department eq 'Engineering'), not $filter/$top.",
"parameters": [
{
"$ref": "#/components/parameters/InstanceId"
},
{
"in": "query",
"name": "pageToken",
"schema": {
"type": "string"
},
"description": "Opaque paging token — pass the previous response's next_page_token."
},
{
"in": "query",
"name": "pageSize",
"schema": {
"type": "integer",
"default": 1000
},
"description": "Maximum users to return in one page (default 1000)."
},
{
"in": "query",
"name": "filter",
"schema": {
"type": "string"
},
"description": "Filter expression over person fields. Operators eq/ne/gt/lt, combined with and/or and parentheses. Example: \"department eq 'Engineering'\"."
}
],
"responses": {
"200": {
"description": "A page of users",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"results": {
"type": "array",
"items": {
"$ref": "#/components/schemas/IdentityGatewayUser"
}
},
"next_page_token": {
"type": "string",
"description": "Token for the next page, or empty on the last page."
}
}
}
}
}
},
"400": {
"description": "Invalid filter expression"
}
}
}
GET /users/{userId}
Get one user in the Moveworks Identity Gateway shape
Emulates the Moveworks Identity Gateway 'Get User by ID' contract. userId is the user's system_identity.id, which is the same value as the directory's person id (e.g. PERSON-0001). Returns one nested gateway user record with no results envelope. Prefer identity__get__people unless you specifically need the gateway wire shape.
Authentication
[
{
"patAuth": []
}
]
Request, responses and operation details
{
"x-odata": false,
"summary": "Get one user in the Moveworks Identity Gateway shape",
"description": "Emulates the Moveworks Identity Gateway 'Get User by ID' contract. userId is the user's system_identity.id, which is the same value as the directory's person id (e.g. PERSON-0001). Returns one nested gateway user record with no results envelope. Prefer identity__get__people unless you specifically need the gateway wire shape.",
"parameters": [
{
"$ref": "#/components/parameters/InstanceId"
},
{
"in": "path",
"name": "userId",
"required": true,
"schema": {
"type": "string"
},
"description": "The user's system_identity.id (e.g. PERSON-0001)."
}
],
"responses": {
"200": {
"description": "The user",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/IdentityGatewayUser"
}
}
}
},
"404": {
"description": "User not found"
}
}
}