---
**Acting as a user (`X-PS-Impersonate-User`).** Every endpoint accepts an optional `X-PS-Impersonate-User` request header whose value is the email of the instance user to act as. The effective user resolves in this order: the header if present, else the instance's configured default MCP user, else none. User-scoped paths additionally accept the literal `me` in place of an email, and Graph-style apps also serve `/me/…` twins of their `/users/{email}/…` paths that resolve the same way. This header is honored by all operations but is intentionally omitted from each operation's parameter list so it is not surfaced as a per-tool argument by MCP clients; see the `ImpersonateUser` entry under `components.parameters`.
---
**Acting as a user (`X-PS-Impersonate-User`).** Every endpoint accepts an optional `X-PS-Impersonate-User` request header whose value is the email of the instance user to act as. The effective user resolves in this order: the header if present, else the instance's configured default MCP user, else none. User-scoped paths additionally accept the literal `me` in place of an email, and Graph-style apps also serve `/me/…` twins of their `/users/{email}/…` paths that resolve the same way. This header is honored by all operations but is intentionally omitted from each operation's parameter list so it is not surfaced as a per-tool argument by MCP clients; see the `ImpersonateUser` entry under `components.parameters`.
Operations
GET /roles
List available ATS hiring roles by department
Resolve a requisition role after selecting a department. Use department=Engineering or $filter=department eq 'Engineering' and active eq true. Re-query after the department changes; use the returned title and department in the requisition. OData query parameters use a leading underscore instead of $ in MCP tools.
Authentication
[
{
"patAuth": []
}
]
Request, responses and operation details
{
"summary": "List available ATS hiring roles by department",
"description": "Resolve a requisition role after selecting a department. Use department=Engineering or $filter=department eq 'Engineering' and active eq true. Re-query after the department changes; use the returned title and department in the requisition. OData query parameters use a leading underscore instead of $ in MCP tools.",
"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": "department",
"required": false,
"schema": {
"type": "string"
},
"example": "Engineering",
"description": "Exact department name. Omit to list roles across all departments. Unknown departments return an empty list."
}
],
"responses": {
"200": {
"description": "Paginated list",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"data": {
"type": "array",
"items": {
"$ref": "#/components/schemas/Role"
}
},
"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 /roles/{id}
Get an ATS hiring role
Read a role's title, department, and active flag by its stable catalog ID.
Authentication
[
{
"patAuth": []
}
]
Request, responses and operation details
{
"summary": "Get an ATS hiring role",
"description": "Read a role's title, department, and active flag by its stable catalog 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/Role"
}
}
}
},
"404": {
"description": "Not found"
}
}
}
GET /departments
List ATS hiring departments
Distinct departments represented in this instance's role catalog, including inactive roles. Pass the returned name to GET /roles?department=... and use $filter=active eq true for active role choices. Supports OData search, filtering, ordering, and pagination. When @odata.truncated is true, the department list and total reflect only the scanned portion of the role catalog.
Authentication
[
{
"patAuth": []
}
]
Request, responses and operation details
{
"summary": "List ATS hiring departments",
"description": "Distinct departments represented in this instance's role catalog, including inactive roles. Pass the returned name to GET /roles?department=... and use $filter=active eq true for active role choices. Supports OData search, filtering, ordering, and pagination. When @odata.truncated is true, the department list and total reflect only the scanned portion of the role catalog.",
"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/HiringDepartment"
}
},
"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 /requisitions
List Purple ATS job requisitions
Approved openings to hire against in Purple ATS — title, department, headcount, hiring manager, required skills and status (open → in_progress/on_hold → filled/closed/cancelled). Candidates and job postings link back to a requisition by requisitionId. Supports server-side querying with _filter/_search/_orderby/_select/_top/_skip/_count (OData $-params, renamed with a leading underscore for MCP).
Authentication
[
{
"patAuth": []
}
]
Request, responses and operation details
{
"summary": "List Purple ATS job requisitions",
"description": "Approved openings to hire against in Purple ATS — title, department, headcount, hiring manager, required skills and status (open → in_progress/on_hold → filled/closed/cancelled). Candidates and job postings link back to a requisition by requisitionId. Supports server-side querying with _filter/_search/_orderby/_select/_top/_skip/_count (OData $-params, renamed with a leading underscore for MCP).",
"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/Requisition"
}
},
"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 /requisitions
Open a job requisition
Creates a new requisition (a request to hire) in Purple ATS. This opens headcount only — it does not publish an advert (create a job posting for that) and does not add applicants.
Authentication
[
{
"patAuth": []
}
]
Request, responses and operation details
{
"summary": "Open a job requisition",
"description": "Creates a new requisition (a request to hire) in Purple ATS. This opens headcount only — it does not publish an advert (create a job posting for that) and does not add applicants.",
"parameters": [
{
"$ref": "#/components/parameters/InstanceId"
}
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Requisition"
}
}
}
},
"responses": {
"201": {
"description": "Created",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Requisition"
}
}
}
}
}
}
GET /requisitions/{id}
Get requisition
Authentication
[
{
"patAuth": []
}
]
Request, responses and operation details
{
"summary": "Get requisition",
"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/Requisition"
}
}
}
},
"404": {
"description": "Not found"
}
}
}
PATCH /requisitions/{id}
Update requisition
Authentication
[
{
"patAuth": []
}
]
Request, responses and operation details
{
"summary": "Update requisition",
"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/Requisition"
}
}
}
},
"responses": {
"200": {
"description": "Updated",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Requisition"
}
}
}
},
"404": {
"description": "Not found"
}
}
}
DELETE /requisitions/{id}
Delete requisition
Authentication
[
{
"patAuth": []
}
]
Request, responses and operation details
{
"summary": "Delete requisition",
"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 /candidates
List Purple ATS job applicants
External people who applied to a Purple ATS requisition, with their pipeline stage (applied → screening → phone_screen → interview → offer → hired, or rejected/withdrawn), source and resume link. These are applicants, not employees (see the hris app) or chat/identity users. Supports server-side querying with _filter/_search/_orderby/_select/_top/_skip/_count (OData $-params, renamed with a leading underscore for MCP).
Authentication
[
{
"patAuth": []
}
]
Request, responses and operation details
{
"summary": "List Purple ATS job applicants",
"description": "External people who applied to a Purple ATS requisition, with their pipeline stage (applied → screening → phone_screen → interview → offer → hired, or rejected/withdrawn), source and resume link. These are applicants, not employees (see the hris app) or chat/identity users. Supports server-side querying with _filter/_search/_orderby/_select/_top/_skip/_count (OData $-params, renamed with a leading underscore for MCP).",
"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/Candidate"
}
},
"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 /candidates
Add an applicant to the hiring pipeline
Registers a new candidate against a Purple ATS requisition, starting at the 'applied' stage unless a status is supplied.
Authentication
[
{
"patAuth": []
}
]
Request, responses and operation details
{
"summary": "Add an applicant to the hiring pipeline",
"description": "Registers a new candidate against a Purple ATS requisition, starting at the 'applied' stage unless a status is supplied.",
"parameters": [
{
"$ref": "#/components/parameters/InstanceId"
}
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Candidate"
}
}
}
},
"responses": {
"201": {
"description": "Created",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Candidate"
}
}
}
}
}
}
GET /candidates/{id}
Get candidate
Authentication
[
{
"patAuth": []
}
]
Request, responses and operation details
{
"summary": "Get candidate",
"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/Candidate"
}
}
}
},
"404": {
"description": "Not found"
}
}
}
PATCH /candidates/{id}
Update a candidate record
Partially updates an applicant in Purple ATS (contact details, source, resume). This is also how a candidate is rejected or withdrawn — set status accordingly. Forward pipeline moves should use the candidate advance tool, which validates the transition and records stage history.
Authentication
[
{
"patAuth": []
}
]
Request, responses and operation details
{
"summary": "Update a candidate record",
"description": "Partially updates an applicant in Purple ATS (contact details, source, resume). This is also how a candidate is rejected or withdrawn — set status accordingly. Forward pipeline moves should use the candidate advance tool, which validates the transition and records stage history.",
"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/Candidate"
}
}
}
},
"responses": {
"200": {
"description": "Updated",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Candidate"
}
}
}
},
"404": {
"description": "Not found"
}
}
}
DELETE /candidates/{id}
Delete candidate
Authentication
[
{
"patAuth": []
}
]
Request, responses and operation details
{
"summary": "Delete candidate",
"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 /candidates/{id}/advance
Advance a candidate to the next hiring stage
Moves a Purple ATS candidate forward through applied → screening → phone_screen → interview → offer → hired (auto-picks the next stage when no target is given) and appends an entry to stageHistory. Forward-only: it refuses backwards moves and candidates who are rejected, withdrawn or already hired — use the candidate update tool to reject or withdraw someone.
Authentication
[
{
"patAuth": []
}
]
Request, responses and operation details
{
"summary": "Advance a candidate to the next hiring stage",
"description": "Moves a Purple ATS candidate forward through applied → screening → phone_screen → interview → offer → hired (auto-picks the next stage when no target is given) and appends an entry to stageHistory. Forward-only: it refuses backwards moves and candidates who are rejected, withdrawn or already hired — use the candidate update tool to reject or withdraw someone.",
"parameters": [
{
"$ref": "#/components/parameters/InstanceId"
},
{
"in": "path",
"name": "id",
"required": true,
"schema": {
"type": "string"
}
}
],
"requestBody": {
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"status": {
"type": "string",
"enum": [
"applied",
"screening",
"phone_screen",
"interview",
"offer",
"hired"
],
"description": "Target stage to advance to. Omit to auto-advance to the next stage. Alias: stage (snake_case/legacy accepted).",
"example": "interview"
},
"notes": {
"type": "string",
"nullable": true,
"example": "Passed phone screen with flying colors."
}
}
}
}
}
},
"responses": {
"200": {
"description": "Candidate advanced",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Candidate"
}
}
}
},
"400": {
"description": "Invalid stage value"
},
"404": {
"description": "Candidate not found"
},
"422": {
"description": "Stage transition not allowed (rejected/withdrawn/hired/backwards move)"
}
}
}
POST /candidates/{id}/generate-offer
Generate a draft offer for a candidate
Creates an Offer record in draft status for a Purple ATS candidate and moves that candidate to the 'offer' stage in one step — prefer this over creating an offer record directly. Rejected if the candidate is already hired or already has a draft/sent offer.
Authentication
[
{
"patAuth": []
}
]
Request, responses and operation details
{
"summary": "Generate a draft offer for a candidate",
"description": "Creates an Offer record in draft status for a Purple ATS candidate and moves that candidate to the 'offer' stage in one step — prefer this over creating an offer record directly. Rejected if the candidate is already hired or already has a draft/sent offer.",
"parameters": [
{
"$ref": "#/components/parameters/InstanceId"
},
{
"in": "path",
"name": "id",
"required": true,
"schema": {
"type": "string"
}
}
],
"requestBody": {
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"salary": {
"type": "number",
"nullable": true,
"example": 130000,
"description": "Base salary. Alias: baseSalary (camelCase accepted). Stored as baseSalary on the created Offer document."
},
"equity": {
"type": "string",
"nullable": true,
"example": "0.25%",
"description": "Equity grant string (e.g. '0.25%')."
},
"start_date": {
"type": "string",
"format": "date-time",
"nullable": true,
"example": "2026-07-14T00:00:00.000Z",
"description": "Proposed start date. Alias: startDate (camelCase accepted)."
},
"expires_at": {
"type": "string",
"format": "date-time",
"nullable": true,
"example": "2026-06-20T00:00:00.000Z",
"description": "Offer expiry date. Alias: expiresAt (camelCase accepted)."
},
"sentAt": {
"type": "string",
"format": "date-time",
"nullable": true,
"example": "2026-06-01T00:00:00.000Z",
"description": "Timestamp when offer was sent, if already dispatched."
}
}
}
}
}
},
"responses": {
"201": {
"description": "Offer created",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Offer"
}
}
}
},
"404": {
"description": "Candidate not found"
},
"422": {
"description": "Candidate already hired or already has an active offer"
}
}
}
GET /interviews
List Purple ATS candidate interviews
Interview sessions booked or completed for Purple ATS candidates — interviewer email, type (phone_screen | technical | behavioral | panel | final), scheduledAt, status and the written feedback plus 1–5 rating. These are hiring loops, not general calendar meetings (see the calendar app). Supports server-side querying with _filter/_search/_orderby/_select/_top/_skip/_count (OData $-params, renamed with a leading underscore for MCP).
Authentication
[
{
"patAuth": []
}
]
Request, responses and operation details
{
"summary": "List Purple ATS candidate interviews",
"description": "Interview sessions booked or completed for Purple ATS candidates — interviewer email, type (phone_screen | technical | behavioral | panel | final), scheduledAt, status and the written feedback plus 1–5 rating. These are hiring loops, not general calendar meetings (see the calendar app). Supports server-side querying with _filter/_search/_orderby/_select/_top/_skip/_count (OData $-params, renamed with a leading underscore for MCP).",
"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/Interview"
}
},
"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 /interviews
Create interview
Authentication
[
{
"patAuth": []
}
]
Request, responses and operation details
{
"summary": "Create interview",
"parameters": [
{
"$ref": "#/components/parameters/InstanceId"
}
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Interview"
}
}
}
},
"responses": {
"201": {
"description": "Created",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Interview"
}
}
}
}
}
}
GET /interviews/{id}
Get interview
Authentication
[
{
"patAuth": []
}
]
Request, responses and operation details
{
"summary": "Get interview",
"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/Interview"
}
}
}
},
"404": {
"description": "Not found"
}
}
}
PATCH /interviews/{id}
Update interview
Authentication
[
{
"patAuth": []
}
]
Request, responses and operation details
{
"summary": "Update interview",
"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/Interview"
}
}
}
},
"responses": {
"200": {
"description": "Updated",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Interview"
}
}
}
},
"404": {
"description": "Not found"
}
}
}
DELETE /interviews/{id}
Delete interview
Authentication
[
{
"patAuth": []
}
]
Request, responses and operation details
{
"summary": "Delete interview",
"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 /offers
List Purple ATS job offers
Employment offers extended to Purple ATS candidates — base salary, equity, start date, expiry and status (draft → sent → accepted/declined/expired/withdrawn). Check here for an existing draft/sent offer before generating another for the same candidate. Supports server-side querying with _filter/_search/_orderby/_select/_top/_skip/_count (OData $-params, renamed with a leading underscore for MCP).
Authentication
[
{
"patAuth": []
}
]
Request, responses and operation details
{
"summary": "List Purple ATS job offers",
"description": "Employment offers extended to Purple ATS candidates — base salary, equity, start date, expiry and status (draft → sent → accepted/declined/expired/withdrawn). Check here for an existing draft/sent offer before generating another for the same candidate. Supports server-side querying with _filter/_search/_orderby/_select/_top/_skip/_count (OData $-params, renamed with a leading underscore for MCP).",
"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/Offer"
}
},
"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 /offers
Create offer
Authentication
[
{
"patAuth": []
}
]
Request, responses and operation details
{
"summary": "Create offer",
"parameters": [
{
"$ref": "#/components/parameters/InstanceId"
}
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Offer"
}
}
}
},
"responses": {
"201": {
"description": "Created",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Offer"
}
}
}
}
}
}
GET /offers/{id}
Get offer
Authentication
[
{
"patAuth": []
}
]
Request, responses and operation details
{
"summary": "Get offer",
"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/Offer"
}
}
}
},
"404": {
"description": "Not found"
}
}
}
PATCH /offers/{id}
Update offer
Authentication
[
{
"patAuth": []
}
]
Request, responses and operation details
{
"summary": "Update offer",
"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/Offer"
}
}
}
},
"responses": {
"200": {
"description": "Updated",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Offer"
}
}
}
},
"404": {
"description": "Not found"
}
}
}
DELETE /offers/{id}
Delete offer
Authentication
[
{
"patAuth": []
}
]
Request, responses and operation details
{
"summary": "Delete offer",
"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 /job-postings
List Purple ATS job postings
Externally-facing job adverts in Purple ATS — title, location, employment type, applicant count and status (draft | active | paused | closed) — usually linked to a requisition. The posting is the advert; the requisition is the approved headcount behind it. Supports server-side querying with _filter/_search/_orderby/_select/_top/_skip/_count (OData $-params, renamed with a leading underscore for MCP).
Authentication
[
{
"patAuth": []
}
]
Request, responses and operation details
{
"summary": "List Purple ATS job postings",
"description": "Externally-facing job adverts in Purple ATS — title, location, employment type, applicant count and status (draft | active | paused | closed) — usually linked to a requisition. The posting is the advert; the requisition is the approved headcount behind it. Supports server-side querying with _filter/_search/_orderby/_select/_top/_skip/_count (OData $-params, renamed with a leading underscore for MCP).",
"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/JobPosting"
}
},
"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 /job-postings
Create job posting
Authentication
[
{
"patAuth": []
}
]
Request, responses and operation details
{
"summary": "Create job posting",
"parameters": [
{
"$ref": "#/components/parameters/InstanceId"
}
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/JobPosting"
}
}
}
},
"responses": {
"201": {
"description": "Created",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/JobPosting"
}
}
}
}
}
}
GET /job-postings/{id}
Get job posting
Authentication
[
{
"patAuth": []
}
]
Request, responses and operation details
{
"summary": "Get job posting",
"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/JobPosting"
}
}
}
},
"404": {
"description": "Not found"
}
}
}
PATCH /job-postings/{id}
Update job posting
Authentication
[
{
"patAuth": []
}
]
Request, responses and operation details
{
"summary": "Update job posting",
"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/JobPosting"
}
}
}
},
"responses": {
"200": {
"description": "Updated",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/JobPosting"
}
}
}
},
"404": {
"description": "Not found"
}
}
}
DELETE /job-postings/{id}
Delete job posting
Authentication
[
{
"patAuth": []
}
]
Request, responses and operation details
{
"summary": "Delete job posting",
"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 /referrals
List Purple ATS employee referrals
Candidates put forward by existing employees, with a per-status tally. Each referral records who referred whom (referrer_email → candidate_name/candidate_email), the position, and its status (pending | reviewing | hired | rejected). A referral is a lead, not yet a candidate in the pipeline — see ats__list__candidates for candidates under active consideration. Returns the whole collection with counts; it does not take OData query params.
Authentication
[
{
"patAuth": []
}
]
Request, responses and operation details
{
"x-odata": false,
"summary": "List Purple ATS employee referrals",
"description": "Candidates put forward by existing employees, with a per-status tally. Each referral records who referred whom (referrer_email → candidate_name/candidate_email), the position, and its status (pending | reviewing | hired | rejected). A referral is a lead, not yet a candidate in the pipeline — see ats__list__candidates for candidates under active consideration. Returns the whole collection with counts; it does not take OData query params.",
"parameters": [
{
"$ref": "#/components/parameters/InstanceId"
}
],
"responses": {
"200": {
"description": "Referral list",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"referrals": {
"type": "array",
"items": {
"type": "object"
}
},
"totalCount": {
"type": "integer"
},
"statusCounts": {
"type": "object"
}
}
}
}
}
}
}
}
POST /referrals
Submit an employee referral
Records a candidate referred by an existing employee and stores it in the instance's referrals collection with status `pending`. referrer_email, candidate_name and candidate_email are required. This creates a referral lead only — it does not add anyone to the hiring pipeline; use ats__create__candidates for that.
Authentication
[
{
"patAuth": []
}
]
Request, responses and operation details
{
"summary": "Submit an employee referral",
"description": "Records a candidate referred by an existing employee and stores it in the instance's referrals collection with status `pending`. referrer_email, candidate_name and candidate_email are required. This creates a referral lead only — it does not add anyone to the hiring pipeline; use ats__create__candidates for that.",
"parameters": [
{
"$ref": "#/components/parameters/InstanceId"
}
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"referrer_email": {
"type": "string",
"format": "email",
"example": "employee@example.com"
},
"candidate_name": {
"type": "string",
"example": "Jane Smith"
},
"candidate_email": {
"type": "string",
"format": "email",
"example": "jane.smith@email.com"
},
"position": {
"type": "string",
"example": "Software Engineer"
}
},
"required": [
"referrer_email",
"candidate_name",
"candidate_email"
]
}
}
}
},
"responses": {
"200": {
"description": "Referral submitted",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"success": {
"type": "boolean"
},
"referral": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"referrer_email": {
"type": "string"
},
"candidate_name": {
"type": "string"
},
"candidate_email": {
"type": "string"
},
"position": {
"type": "string"
},
"status": {
"type": "string"
},
"created_at": {
"type": "string",
"format": "date-time"
}
}
}
}
}
}
}
},
"400": {
"description": "Missing required fields"
}
}
}