Purple Suite Access API OpenAPI

Identity governance and access requests. Resembles tools like SailPoint, Saviynt and Okta Identity Governance: a catalog of internal systems, each with its own roles and its own environments; access requests that name a system, role and environment; security attestations filed against a request; and provisioning grants. Production environments require an attestation before provisioning, but no endpoint enforces that — each endpoint validates its own payload only, and GET /access-status/{requestId} reports what is on record. --- **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`.

This reference is readable without JavaScript. Download the OpenAPI document.

Runtime

/api/purple-suite/access

Identity governance and access requests. Resembles tools like SailPoint, Saviynt and Okta Identity Governance: a catalog of internal systems, each with its own roles and its own environments; access requests that name a system, role and environment; security attestations filed against a request; and provisioning grants. Production environments require an attestation before provisioning, but no endpoint enforces that — each endpoint validates its own payload only, and GET /access-status/{requestId} reports what is on record. --- **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/access"
  }
]

Authentication schemes

{
  "patAuth": {
    "type": "http",
    "scheme": "bearer"
  }
}

Each operation lists its security requirements. An empty security array permits an unauthenticated request. Use the scheme and headers declared for the selected contract.

Operations

GET /systems

List systems access can be requested for

The catalog of internal systems. Each system has its own roles (GET /systems/{systemId}/roles) and its own environments (GET /systems/{systemId}/environments); ids from those lists are what an access request expects.

Authentication

[
  {
    "patAuth": []
  }
]

Request, responses and operation details

{
  "summary": "List systems access can be requested for",
  "description": "The catalog of internal systems. Each system has its own roles (GET /systems/{systemId}/roles) and its own environments (GET /systems/{systemId}/environments); ids from those lists are what an access request expects.",
  "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/AccessSystem"
                }
              },
              "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)"
    }
  }
}

GET /systems/{id}

Get a system

Reads one system by systemId.

Authentication

[
  {
    "patAuth": []
  }
]

Request, responses and operation details

{
  "summary": "Get a system",
  "description": "Reads one system by systemId.",
  "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/AccessSystem"
          }
        }
      }
    },
    "404": {
      "description": "Not found"
    }
  }
}

GET /systems/{systemId}/roles

List the roles available on a system

Roles differ per system and role ids do not overlap between systems. Use the id of one of these as roleId on an access request for this system. 404 for an unknown systemId.

Authentication

[
  {
    "patAuth": []
  }
]

Request, responses and operation details

{
  "operationId": "access__list__system_roles",
  "summary": "List the roles available on a system",
  "description": "Roles differ per system and role ids do not overlap between systems. Use the id of one of these as roleId on an access request for this system. 404 for an unknown systemId.",
  "parameters": [
    {
      "in": "path",
      "name": "systemId",
      "required": true,
      "schema": {
        "type": "string"
      },
      "description": "id of a system from GET /systems."
    },
    {
      "$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/SystemRole"
                }
              },
              "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)"
    },
    "404": {
      "description": "Unknown systemId"
    }
  }
}

GET /systems/{systemId}/environments

List the environments available on a system

Environments differ per system. Each says whether it requiresAttestation: when true, a security attestation must be filed on the request (POST /requests/{id}/attestations) before it is provisioned. 404 for an unknown systemId.

Authentication

[
  {
    "patAuth": []
  }
]

Request, responses and operation details

{
  "operationId": "access__list__system_environments",
  "summary": "List the environments available on a system",
  "description": "Environments differ per system. Each says whether it requiresAttestation: when true, a security attestation must be filed on the request (POST /requests/{id}/attestations) before it is provisioned. 404 for an unknown systemId.",
  "parameters": [
    {
      "in": "path",
      "name": "systemId",
      "required": true,
      "schema": {
        "type": "string"
      },
      "description": "id of a system from GET /systems."
    },
    {
      "$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/SystemEnvironment"
                }
              },
              "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)"
    },
    "404": {
      "description": "Unknown systemId"
    }
  }
}

GET /requests

List access requests

Access requests in Purple Access. Filter with requesterEmail, systemId or status, or with _filter (e.g. status eq 'open').

Authentication

[
  {
    "patAuth": []
  }
]

Request, responses and operation details

{
  "summary": "List access requests",
  "description": "Access requests in Purple Access. Filter with requesterEmail, systemId or status, or with _filter (e.g. status eq 'open').",
  "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."
    },
    {
      "in": "query",
      "name": "requesterEmail",
      "schema": {
        "type": "string"
      },
      "description": "Exact-match filter on requesterEmail."
    },
    {
      "in": "query",
      "name": "systemId",
      "schema": {
        "type": "string"
      },
      "description": "Exact-match filter on systemId."
    },
    {
      "in": "query",
      "name": "status",
      "schema": {
        "type": "string",
        "enum": [
          "open",
          "provisioned"
        ]
      },
      "description": "Exact-match filter on status."
    }
  ],
  "responses": {
    "200": {
      "description": "Paginated list",
      "content": {
        "application/json": {
          "schema": {
            "type": "object",
            "properties": {
              "data": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/AccessRequest"
                }
              },
              "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 /requests

Create an access request

Opens an access request. Requires requesterEmail, systemId (must exist), roleId, environmentId and justification. roleId and environmentId are recorded as given: this endpoint does not check that they belong to the system, so take them from the system's own role and environment lists. Returns the request with status "open"; its id is what attestations, provisioning and access-status expect.

Authentication

[
  {
    "patAuth": []
  }
]

Request, responses and operation details

{
  "summary": "Create an access request",
  "description": "Opens an access request. Requires requesterEmail, systemId (must exist), roleId, environmentId and justification. roleId and environmentId are recorded as given: this endpoint does not check that they belong to the system, so take them from the system's own role and environment lists. Returns the request with status \"open\"; its id is what attestations, provisioning and access-status expect.",
  "parameters": [
    {
      "$ref": "#/components/parameters/InstanceId"
    }
  ],
  "requestBody": {
    "required": true,
    "content": {
      "application/json": {
        "schema": {
          "$ref": "#/components/schemas/AccessRequest"
        }
      }
    }
  },
  "responses": {
    "201": {
      "description": "Created",
      "content": {
        "application/json": {
          "schema": {
            "$ref": "#/components/schemas/AccessRequest"
          }
        }
      }
    }
  }
}

GET /requests/{id}

Get an access request

Reads one access request by id.

Authentication

[
  {
    "patAuth": []
  }
]

Request, responses and operation details

{
  "summary": "Get an access request",
  "description": "Reads one access request by id.",
  "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/AccessRequest"
          }
        }
      }
    },
    "404": {
      "description": "Not found"
    }
  }
}

PATCH /requests/{id}

Update an access request

Changes systemId, roleId, environmentId, justification or requesterEmail on an existing request (recorded as given; systemId must exist). Attestations already on the request keep the system + environment they were filed for, so a change of system or environment leaves them stale. Bumps updatedAt.

Authentication

[
  {
    "patAuth": []
  }
]

Request, responses and operation details

{
  "summary": "Update an access request",
  "description": "Changes systemId, roleId, environmentId, justification or requesterEmail on an existing request (recorded as given; systemId must exist). Attestations already on the request keep the system + environment they were filed for, so a change of system or environment leaves them stale. Bumps updatedAt.",
  "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/AccessRequest"
        }
      }
    }
  },
  "responses": {
    "200": {
      "description": "Updated",
      "content": {
        "application/json": {
          "schema": {
            "$ref": "#/components/schemas/AccessRequest"
          }
        }
      }
    },
    "404": {
      "description": "Not found"
    }
  }
}

POST /requests/{id}/attestations

File a security attestation on an access request

Puts a security attestation on the request. The attestation snapshots the request's current systemId and environmentId; it covers only that combination. Requires statement and attestedBy. 404 for an unknown request id.

Authentication

[
  {
    "patAuth": []
  }
]

Request, responses and operation details

{
  "operationId": "access__create__request_attestations",
  "summary": "File a security attestation on an access request",
  "description": "Puts a security attestation on the request. The attestation snapshots the request's current systemId and environmentId; it covers only that combination. Requires statement and attestedBy. 404 for an unknown request id.",
  "parameters": [
    {
      "$ref": "#/components/parameters/InstanceId"
    },
    {
      "in": "path",
      "name": "id",
      "required": true,
      "schema": {
        "type": "string"
      },
      "description": "id of an existing access request."
    }
  ],
  "requestBody": {
    "required": true,
    "content": {
      "application/json": {
        "schema": {
          "$ref": "#/components/schemas/AttestationInput"
        }
      }
    }
  },
  "responses": {
    "201": {
      "description": "Filed",
      "content": {
        "application/json": {
          "schema": {
            "$ref": "#/components/schemas/Attestation"
          }
        }
      }
    },
    "400": {
      "description": "Missing statement or attestedBy"
    },
    "404": {
      "description": "Unknown request id"
    }
  }
}

POST /requests/{id}/provision

Provision the access on a request

Grants the access the request currently describes. The grant snapshots systemId, roleId and environmentId at grant time and sets the request's status to "provisioned". This endpoint validates only its own payload: it does not check for an attestation or whether the role and environment belong to the system. Optional note. 404 for an unknown request id.

Authentication

[
  {
    "patAuth": []
  }
]

Request, responses and operation details

{
  "operationId": "access__create__request_provision",
  "summary": "Provision the access on a request",
  "description": "Grants the access the request currently describes. The grant snapshots systemId, roleId and environmentId at grant time and sets the request's status to \"provisioned\". This endpoint validates only its own payload: it does not check for an attestation or whether the role and environment belong to the system. Optional note. 404 for an unknown request id.",
  "parameters": [
    {
      "$ref": "#/components/parameters/InstanceId"
    },
    {
      "in": "path",
      "name": "id",
      "required": true,
      "schema": {
        "type": "string"
      },
      "description": "id of an existing access request."
    }
  ],
  "requestBody": {
    "required": false,
    "content": {
      "application/json": {
        "schema": {
          "$ref": "#/components/schemas/ProvisionInput"
        }
      }
    }
  },
  "responses": {
    "201": {
      "description": "Provisioned",
      "content": {
        "application/json": {
          "schema": {
            "$ref": "#/components/schemas/ProvisioningGrant"
          }
        }
      }
    },
    "404": {
      "description": "Unknown request id"
    }
  }
}

GET /attestations

List security attestations

Attestations filed on access requests, each with the systemId + environmentId it was filed for. Filter with requestId to see what covers one request.

Authentication

[
  {
    "patAuth": []
  }
]

Request, responses and operation details

{
  "summary": "List security attestations",
  "description": "Attestations filed on access requests, each with the systemId + environmentId it was filed for. Filter with requestId to see what covers one request.",
  "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/Attestation"
                }
              },
              "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)"
    }
  }
}

GET /attestations/{id}

Get an attestation

Reads one attestation by id.

Authentication

[
  {
    "patAuth": []
  }
]

Request, responses and operation details

{
  "summary": "Get an attestation",
  "description": "Reads one attestation by id.",
  "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/Attestation"
          }
        }
      }
    },
    "404": {
      "description": "Not found"
    }
  }
}

GET /grants

List provisioning grants

Grants issued by POST /requests/{id}/provision, each with the systemId, roleId and environmentId it was issued for. Filter with requestId.

Authentication

[
  {
    "patAuth": []
  }
]

Request, responses and operation details

{
  "summary": "List provisioning grants",
  "description": "Grants issued by POST /requests/{id}/provision, each with the systemId, roleId and environmentId it was issued for. Filter with requestId.",
  "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/ProvisioningGrant"
                }
              },
              "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)"
    }
  }
}

GET /grants/{id}

Get a provisioning grant

Reads one grant by id.

Authentication

[
  {
    "patAuth": []
  }
]

Request, responses and operation details

{
  "summary": "Get a provisioning grant",
  "description": "Reads one grant by id.",
  "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/ProvisioningGrant"
          }
        }
      }
    },
    "404": {
      "description": "Not found"
    }
  }
}

GET /access-status/{requestId}

Get what is on record for an access request

What is actually on record for one access request: the current record, every attestation filed on it (each with the systemId + environmentId it was filed for), every provisioning grant issued on it (each with the systemId, roleId and environmentId it was issued for), and which steps have records. Facts only; nothing here is evaluated. Use this to verify what is on record before telling the requester anything.

Authentication

[
  {
    "patAuth": []
  }
]

Request, responses and operation details

{
  "operationId": "access__get__access_status",
  "summary": "Get what is on record for an access request",
  "description": "What is actually on record for one access request: the current record, every attestation filed on it (each with the systemId + environmentId it was filed for), every provisioning grant issued on it (each with the systemId, roleId and environmentId it was issued for), and which steps have records. Facts only; nothing here is evaluated. Use this to verify what is on record before telling the requester anything.",
  "parameters": [
    {
      "$ref": "#/components/parameters/InstanceId"
    },
    {
      "in": "path",
      "name": "requestId",
      "required": true,
      "schema": {
        "type": "string"
      },
      "description": "id of the access request."
    }
  ],
  "responses": {
    "200": {
      "description": "Access status",
      "content": {
        "application/json": {
          "schema": {
            "$ref": "#/components/schemas/AccessStatus"
          }
        }
      }
    },
    "404": {
      "description": "Unknown request id"
    }
  }
}

GET /requests/{requestId}/verdict

Evaluate the access policy for a request (evaluators only)

Ground truth for graders, not a tool for agents ("x-mcp": false, so the unified MCP server does not derive it). Evaluates the request's current record and every grant's own snapshot against the system catalog and the attestations on file: checks (role and environment validity, whether an attestation is required and whether one matches the current record) plus a single verdict.

Authentication

[
  {
    "patAuth": []
  }
]

Request, responses and operation details

{
  "operationId": "access__get__request_verdict",
  "x-mcp": false,
  "summary": "Evaluate the access policy for a request (evaluators only)",
  "description": "Ground truth for graders, not a tool for agents (\"x-mcp\": false, so the unified MCP server does not derive it). Evaluates the request's current record and every grant's own snapshot against the system catalog and the attestations on file: checks (role and environment validity, whether an attestation is required and whether one matches the current record) plus a single verdict.",
  "parameters": [
    {
      "$ref": "#/components/parameters/InstanceId"
    },
    {
      "in": "path",
      "name": "requestId",
      "required": true,
      "schema": {
        "type": "string"
      },
      "description": "id of the access request."
    }
  ],
  "responses": {
    "200": {
      "description": "Verdict",
      "content": {
        "application/json": {
          "schema": {
            "$ref": "#/components/schemas/AccessVerdict"
          }
        }
      }
    },
    "404": {
      "description": "Unknown request id"
    }
  }
}

Schemas and reusable 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

AccessSystem

{
  "type": "object",
  "properties": {
    "id": {
      "type": "string",
      "readOnly": true,
      "description": "The systemId every other endpoint expects.",
      "example": "data-warehouse"
    },
    "name": {
      "type": "string",
      "example": "Data Warehouse"
    },
    "description": {
      "type": "string"
    },
    "createdAt": {
      "type": "string",
      "format": "date-time",
      "readOnly": true
    },
    "updatedAt": {
      "type": "string",
      "format": "date-time",
      "readOnly": true
    }
  }
}

SystemRole

{
  "type": "object",
  "properties": {
    "id": {
      "type": "string",
      "readOnly": true,
      "description": "The roleId to put on an access request.",
      "example": "analyst"
    },
    "name": {
      "type": "string",
      "example": "Analyst"
    },
    "tier": {
      "type": "string",
      "enum": [
        "standard",
        "elevated"
      ]
    },
    "systemId": {
      "type": "string",
      "example": "data-warehouse"
    },
    "createdAt": {
      "type": "string",
      "format": "date-time",
      "readOnly": true
    },
    "updatedAt": {
      "type": "string",
      "format": "date-time",
      "readOnly": true
    }
  }
}

SystemEnvironment

{
  "type": "object",
  "properties": {
    "id": {
      "type": "string",
      "readOnly": true,
      "description": "The environmentId to put on an access request.",
      "example": "prod"
    },
    "name": {
      "type": "string",
      "example": "Production"
    },
    "requiresAttestation": {
      "type": "boolean",
      "description": "True when a security attestation must be on the request before it is provisioned."
    },
    "systemId": {
      "type": "string",
      "example": "data-warehouse"
    },
    "createdAt": {
      "type": "string",
      "format": "date-time",
      "readOnly": true
    },
    "updatedAt": {
      "type": "string",
      "format": "date-time",
      "readOnly": true
    }
  }
}

AccessRequest

{
  "type": "object",
  "required": [
    "requesterEmail",
    "systemId",
    "roleId",
    "environmentId",
    "justification"
  ],
  "properties": {
    "id": {
      "type": "string",
      "readOnly": true
    },
    "requesterEmail": {
      "type": "string",
      "format": "email",
      "example": "priya.raman@example.com"
    },
    "systemId": {
      "type": "string",
      "description": "Must be the id of a system from GET /systems.",
      "example": "data-warehouse"
    },
    "roleId": {
      "type": "string",
      "description": "Recorded as given. Take it from GET /systems/{systemId}/roles for the chosen system.",
      "example": "analyst"
    },
    "environmentId": {
      "type": "string",
      "description": "Recorded as given. Take it from GET /systems/{systemId}/environments for the chosen system.",
      "example": "prod"
    },
    "justification": {
      "type": "string",
      "example": "Q4 revenue dashboards for project PX-1234."
    },
    "status": {
      "type": "string",
      "enum": [
        "open",
        "provisioned"
      ],
      "readOnly": true,
      "description": "\"open\" until POST /requests/{id}/provision succeeds, then \"provisioned\"."
    },
    "createdAt": {
      "type": "string",
      "format": "date-time",
      "readOnly": true
    },
    "updatedAt": {
      "type": "string",
      "format": "date-time",
      "readOnly": true
    }
  }
}

AttestationInput

{
  "type": "object",
  "required": [
    "statement",
    "attestedBy"
  ],
  "properties": {
    "statement": {
      "type": "string",
      "description": "What the requester attests to, in their words.",
      "example": "I completed the data handling training on 2026-09-15 and will only use this access for project PX-1234."
    },
    "attestedBy": {
      "type": "string",
      "format": "email",
      "description": "Email of the person making the attestation.",
      "example": "priya.raman@example.com"
    }
  }
}

Attestation

{
  "type": "object",
  "properties": {
    "id": {
      "type": "string",
      "readOnly": true
    },
    "requestId": {
      "type": "string",
      "readOnly": true
    },
    "systemId": {
      "type": "string",
      "readOnly": true,
      "description": "Snapshot of the request's systemId when the attestation was filed."
    },
    "environmentId": {
      "type": "string",
      "readOnly": true,
      "description": "Snapshot of the request's environmentId when the attestation was filed."
    },
    "statement": {
      "type": "string",
      "readOnly": true
    },
    "attestedBy": {
      "type": "string",
      "format": "email",
      "readOnly": true
    },
    "createdAt": {
      "type": "string",
      "format": "date-time",
      "readOnly": true
    }
  }
}

ProvisionInput

{
  "type": "object",
  "properties": {
    "note": {
      "type": "string",
      "description": "Optional note recorded on the grant."
    }
  }
}

ProvisioningGrant

{
  "type": "object",
  "properties": {
    "id": {
      "type": "string",
      "readOnly": true
    },
    "requestId": {
      "type": "string",
      "readOnly": true
    },
    "systemId": {
      "type": "string",
      "readOnly": true,
      "description": "Snapshot of the request's systemId at grant time."
    },
    "roleId": {
      "type": "string",
      "readOnly": true,
      "description": "Snapshot of the request's roleId at grant time."
    },
    "environmentId": {
      "type": "string",
      "readOnly": true,
      "description": "Snapshot of the request's environmentId at grant time."
    },
    "grantedAt": {
      "type": "string",
      "format": "date-time",
      "readOnly": true
    },
    "note": {
      "type": "string",
      "readOnly": true
    },
    "createdAt": {
      "type": "string",
      "format": "date-time",
      "readOnly": true
    }
  }
}

AccessStatus

{
  "type": "object",
  "description": "What is on record for one access request. Facts only: no checks and no verdict.",
  "properties": {
    "request": {
      "$ref": "#/components/schemas/AccessRequest"
    },
    "attestations": {
      "type": "array",
      "description": "Every attestation filed on this request, oldest first, each with the systemId + environmentId it was filed for.",
      "items": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "systemId": {
            "type": "string",
            "description": "Snapshot of the request's systemId when the attestation was filed."
          },
          "environmentId": {
            "type": "string",
            "description": "Snapshot of the request's environmentId when the attestation was filed."
          },
          "statement": {
            "type": "string"
          },
          "attestedBy": {
            "type": "string",
            "format": "email"
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          }
        }
      }
    },
    "grants": {
      "type": "array",
      "description": "Every provisioning grant issued on this request, oldest first, each with the systemId, roleId and environmentId it was issued for.",
      "items": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "systemId": {
            "type": "string",
            "description": "Snapshot of the request's systemId at grant time."
          },
          "roleId": {
            "type": "string",
            "description": "Snapshot of the request's roleId at grant time."
          },
          "environmentId": {
            "type": "string",
            "description": "Snapshot of the request's environmentId at grant time."
          },
          "grantedAt": {
            "type": "string",
            "format": "date-time"
          }
        }
      }
    },
    "steps": {
      "type": "object",
      "description": "Which steps have a record for this request, and the ids of those records.",
      "properties": {
        "request": {
          "type": "object",
          "properties": {
            "on_record": {
              "type": "boolean"
            }
          }
        },
        "attestation": {
          "type": "object",
          "properties": {
            "on_record": {
              "type": "boolean"
            },
            "ids": {
              "type": "array",
              "items": {
                "type": "string"
              }
            }
          }
        },
        "provisioning": {
          "type": "object",
          "properties": {
            "on_record": {
              "type": "boolean"
            },
            "ids": {
              "type": "array",
              "items": {
                "type": "string"
              }
            }
          }
        }
      }
    }
  }
}

AccessVerdict

{
  "type": "object",
  "description": "The access policy evaluated against one request. Evaluator ground truth; never derived as an MCP tool.",
  "properties": {
    "request": {
      "$ref": "#/components/schemas/AccessRequest"
    },
    "steps": {
      "type": "object",
      "description": "Which steps have a record for this request.",
      "properties": {
        "request": {
          "type": "object",
          "properties": {
            "on_record": {
              "type": "boolean"
            }
          }
        },
        "attestation": {
          "type": "object",
          "properties": {
            "on_record": {
              "type": "boolean"
            },
            "ids": {
              "type": "array",
              "items": {
                "type": "string"
              }
            },
            "matches_current": {
              "type": "boolean",
              "description": "True when some attestation's snapshot equals the request's current systemId + environmentId."
            }
          }
        },
        "provisioning": {
          "type": "object",
          "properties": {
            "on_record": {
              "type": "boolean"
            },
            "ids": {
              "type": "array",
              "items": {
                "type": "string"
              }
            },
            "unattested_ids": {
              "type": "array",
              "items": {
                "type": "string"
              },
              "description": "Grants whose own snapshot required an attestation that no attestation on this request covers."
            }
          }
        }
      }
    },
    "checks": {
      "type": "object",
      "description": "Evaluated against the request's current record.",
      "properties": {
        "role_valid_for_system": {
          "type": "boolean"
        },
        "environment_valid_for_system": {
          "type": "boolean"
        },
        "attestation_required": {
          "type": "boolean"
        },
        "attestation_satisfied": {
          "type": "boolean",
          "description": "Some attestation on this request has a snapshot equal to the request's current systemId + environmentId."
        }
      }
    },
    "verdict": {
      "type": "string",
      "enum": [
        "OK",
        "PROVISIONED_WITHOUT_ATTESTATION",
        "STALE_ATTESTATION",
        "INVALID_ROLE",
        "INVALID_ENVIRONMENT",
        "NOT_PROVISIONED"
      ],
      "description": "When provisioned, in priority order: INVALID_ROLE, INVALID_ENVIRONMENT, PROVISIONED_WITHOUT_ATTESTATION (required and none filed), STALE_ATTESTATION (required, some filed, none matching the current record or a grant's snapshot), OK. When no grant exists: NOT_PROVISIONED."
    }
  }
}

Document metadata and extensions

{
  "openapi": "3.0.0",
  "info": {
    "title": "Purple Suite Access API",
    "version": "1.0.0",
    "description": "Identity governance and access requests. Resembles tools like SailPoint, Saviynt and Okta Identity Governance: a catalog of internal systems, each with its own roles and its own environments; access requests that name a system, role and environment; security attestations filed against a request; and provisioning grants. Production environments require an attestation before provisioning, but no endpoint enforces that — each endpoint validates its own payload only, and GET /access-status/{requestId} reports what is on record.\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`."
  },
  "security": [
    {
      "patAuth": []
    }
  ]
}
Interactive API explorer (requires JavaScript)