{
  "openapi": "3.0.1",
  "info": {
    "title": "Alex API Documentation",
    "description": "REST API for the Alex hiring platform. Manage jobs, invite candidates to AI interviews, and retrieve interview results and reports.",
    "license": {
      "name": "MIT"
    },
    "version": "1.0.0"
  },
  "servers": [
    {
      "url": "https://api.alex.com/v1/api"
    }
  ],
  "security": [
    {
      "apiKeyAuth": []
    }
  ],
  "paths": {
    "/interviewers": {
      "get": {
        "description": "Returns a list of all the interviewers in the system.",
        "responses": {
          "200": {
            "description": "Interviewers response",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/Interviewer"
                  }
                }
              }
            }
          },
          "400": {
            "description": "Unexpected error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/reports": {
      "get": {
        "description": "Returns a list reports from the system. Can be filtered by the positionId, reportId, or candidateId. Will only return reports that are completed.",
        "parameters": [
          {
            "name": "reportId",
            "in": "query",
            "description": "The id of the report to filter by. If this is provided, all other filters will be ignored.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "positionId",
            "in": "query",
            "description": "The id of the position to filter by",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "candidateId",
            "in": "query",
            "description": "The id of the candidate to filter by",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Reports response",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/Report"
                  }
                }
              }
            }
          },
          "400": {
            "description": "Unexpected error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/interviews": {
      "get": {
        "description": "Returns a list interviews from the system. Can be filtered by the positionId, interviewId, or candidateId. Will only return interviews that are completed.",
        "parameters": [
          {
            "name": "interviewId",
            "in": "query",
            "description": "The id of the interview to filter by. If this is provided, all other filters will be ignored.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "positionId",
            "in": "query",
            "description": "The id of the position to filter by",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "candidateId",
            "in": "query",
            "description": "The id of the candidate to filter by",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Interviews response",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/Interview"
                  }
                }
              }
            }
          },
          "400": {
            "description": "Unexpected error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/positions": {
      "get": {
        "description": "Returns a list of positions from the system. Optional `status` and `label` query parameters narrow the results.",
        "parameters": [
          {
            "name": "status",
            "in": "query",
            "description": "Filter by position status. Accepts \"Active\" or \"Inactive\" (case-insensitive).",
            "schema": {
              "type": "string",
              "enum": ["Active", "Inactive"]
            }
          },
          {
            "name": "label",
            "in": "query",
            "description": "Filter by label name (case-insensitive exact match). Returns positions that have a label with this name attached.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Positions response",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/Position"
                  }
                }
              }
            }
          },
          "400": {
            "description": "Invalid filter value (e.g. unrecognized `status`)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/inviteCandidate": {
      "post": {
        "description": "Invites a candidate to the specified job. The candidate is created if they don't already exist for your company (matched on `candidateExternalId` if provided, otherwise on email/mobile), attached to the job, and the job's Workflows V8 invitation flow is triggered. The target job must have a Workflows V8 configuration; calls against legacy V1 jobs return 400. Provide either `jobExternalId` (the ATS-side identifier you supplied when creating the job) or `interviewerId` (the Alex-side identifier returned from `/createJob` and `/interviewers`) — exactly one. At least one of `email` or `mobile` is required as the candidate contact.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/InviteCandidateRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Candidate invited",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/InviteCandidateResponse"
                }
              }
            }
          },
          "400": {
            "description": "Missing or invalid request fields",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid API key",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "404": {
            "description": "Job not found for the provided identifier",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "500": {
            "description": "Unexpected server error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/createJob": {
      "post": {
        "description": "Starts asynchronous creation of a new job (interviewer) from a job description. Returns immediately with the deterministic Alex `interviewerId` that the job will be assigned once generation completes. Job generation runs in the background; poll `GET /interviewers` to detect when the job is available. The job runs your company's default workflow, and every round of it is created: each Alex interview round gets its own interview guide, generated from the job description unless you supply that round's `questions` and/or `criteria`. Pass `templateId` to apply an interview guide template (from `GET /templates`) instead of the company's default.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreateJobRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Job generation initiated",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CreateJobResponse"
                }
              }
            }
          },
          "400": {
            "description": "Missing required fields, duplicate `externalJobId`, or `questions` / `criteria` that do not fit the workflow's rounds. `message` names the offending field and why.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid API key",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "404": {
            "description": "`templateId` does not match a template in this company",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "422": {
            "description": "Your company has no default workflow configured, or its default workflow has no Alex interview round",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "500": {
            "description": "Unexpected server error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/templates": {
      "get": {
        "description": "Returns interview guide templates for your company. Pass `id` to filter to a single template. Unknown ids return an empty list.",
        "parameters": [
          {
            "name": "id",
            "in": "query",
            "description": "The id of the template to filter by",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Templates response",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/Template"
                  }
                }
              }
            }
          },
          "400": {
            "description": "Invalid filter value",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid API key",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "post": {
        "description": "Creates a new interview guide template. Template names must be unique within your company. The returned `payload` is the template id to pass as `templateId` on `/createJob`.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreateTemplateRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Template created",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CreateTemplateResponse"
                }
              }
            }
          },
          "400": {
            "description": "Missing name, invalid fields, or duplicate template name",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid API key",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "500": {
            "description": "Unexpected server error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/candidates": {
      "get": {
        "description": "Returns a list of candidates from the system. Can be filtered by positionId, candidateId, or email.",
        "parameters": [
          {
            "name": "email",
            "in": "query",
            "description": "The email of the candidate to filter by",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "candidateId",
            "in": "query",
            "description": "The id of the candidate to filter by",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "positionId",
            "in": "query",
            "description": "The id of the position to filter by. Filters on candidates that have been interviewed for this position.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Candidates response",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/Candidate"
                  }
                }
              }
            }
          },
          "400": {
            "description": "Unexpected error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/deleteCandidate": {
      "post": {
        "description": "Permanently deletes a candidate and everything associated with them. This is **irreversible** — see the warning on this page for the full list of what is destroyed.\n\nIdentify the candidate with exactly one of `candidateId`, `candidateExternalId`, or `email`. Supplying zero or more than one returns 400. All lookups are scoped to the company owning the API key, so you can never delete another company's records.\n\nThe response returns a snapshot of each deleted record. This is your only copy: once the call returns, the data is gone from Alex. Note that `assets.cleanupSucceeded` is `true` once the background deletion of stored files has started and `false` if it could not be started (the database records are deleted either way) — alert on it if you must guarantee media removal.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/DeleteCandidateRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Candidate deleted",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DeleteCandidateResponse"
                }
              }
            }
          },
          "400": {
            "description": "Zero or more than one identifier supplied",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid API key",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "404": {
            "description": "No candidate matched the identifier in this company",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          },
          "500": {
            "description": "Unexpected server error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponse"
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "schemas": {
      "Interviewer": {
        "type": "object",
        "properties": {
          "interviewerId": {
            "description": "The unique identifier for the interviewer",
            "type": "string"
          },
          "role": {
            "description": "The name of the role this interviewer is used for",
            "type": "string"
          }
        }
      },
      "Position": {
        "type": "object",
        "properties": {
          "positionId": {
            "description": "The unique identifier for the position",
            "type": "string"
          },
          "name": {
            "description": "The name of this position",
            "type": "string"
          },
          "externalJobId": {
            "description": "The ID of this position's job in the connected ATS, or null if there is no linked ATS record",
            "type": "string",
            "nullable": true,
            "example": "12345"
          }
        }
      },
      "Template": {
        "type": "object",
        "properties": {
          "id": {
            "description": "The unique identifier for the interview guide template",
            "type": "string",
            "example": "clx8f2k1p0000abcd1234efgh"
          },
          "name": {
            "description": "The template name",
            "type": "string",
            "example": "SWE Screen"
          },
          "createdAt": {
            "description": "ISO 8601 creation time",
            "type": "string",
            "format": "date-time",
            "example": "2026-01-02T00:00:00.000Z"
          },
          "updatedAt": {
            "description": "ISO 8601 last update time, or null if never updated",
            "type": "string",
            "format": "date-time",
            "nullable": true,
            "example": "2026-01-03T00:00:00.000Z"
          },
          "estimatedDuration": {
            "description": "Estimated interview duration in minutes",
            "type": "integer",
            "nullable": true,
            "example": 25
          }
        }
      },
      "Report": {
        "type": "object",
        "properties": {
          "reportId": {
            "description": "The unique identifier of the report",
            "type": "string",
            "example": "reportId"
          },
          "candidateId": {
            "description": "The unique identifier of the candidate",
            "type": "string",
            "example": "candidateId"
          },
          "positionId": {
            "description": "The unique identifier of the position",
            "type": "string",
            "example": "positonId"
          },
          "externalJobId": {
            "description": "The ID of the position's job in the connected ATS, or null if there is no linked ATS record",
            "type": "string",
            "nullable": true,
            "example": "12345"
          },
          "overallScore": {
            "description": "The overall score of the candidate",
            "type": "number",
            "example": 85
          },
          "overallFeedback": {
            "description": "Feedback on the candidate's overall performance",
            "type": "string",
            "example": "The candidate performed well overall."
          },
          "questionSummary": {
            "description": "List of dictionaries, each containing the question and a summary of the candidate's answer",
            "type": "array",
            "items": {
              "type": "object",
              "description": "A summary of the candidate's answer to a question",
              "properties": {
                "question": {
                  "description": "The question that was asked",
                  "type": "string",
                  "example": "What is your favorite color?"
                },
                "summary": {
                  "description": "A summary of the candidate's answer to this question",
                  "type": "string",
                  "example": "The candidate's favorite color is blue."
                }
              }
            }
          },
          "tags": {
            "description": "A list of dictionaries, each containing the tag name, value, and evidence",
            "type": "array",
            "items": {
              "type": "object",
              "description": "The tag name, value, and evidence",
              "properties": {
                "name": {
                  "description": "The name of the tag",
                  "type": "string",
                  "example": "Work Authorization"
                },
                "value": {
                  "description": "The assigned value for the given report",
                  "type": "string",
                  "example": "Yes"
                },
                "evidence": {
                  "description": "Any evidence that supports the assigned value",
                  "type": "string",
                  "example": "The candidate confirmed they are authorized to work in the United States"
                }
              }
            }
          },
          "skills": {
            "description": "A list of dictionaries, each containing the skill and the candidate's score and feedback",
            "type": "array",
            "items": {
              "type": "object",
              "description": "The candidate's score and feedback for a skill",
              "properties": {
                "name": {
                  "description": "The skill that was evaluated",
                  "type": "string",
                  "example": "Communication"
                },
                "score": {
                  "description": "The candidate's score for this skill",
                  "type": "number",
                  "example": 85
                },
                "feedback": {
                  "description": "Feedback on the candidate's performance for this skill",
                  "type": "string",
                  "example": "The candidate communicated clearly and effectively."
                }
              }
            }
          },
          "resumeFit": {
            "description": "The resume fit analysis for this candidate and position, or null if no analysis was run",
            "type": "object",
            "nullable": true,
            "properties": {
              "score": {
                "description": "The overall resume fit score, from 0 to 100, or null if the analysis produced no score",
                "type": "number",
                "nullable": true,
                "example": 85
              },
              "currentRole": {
                "description": "The candidate's current role as identified from their resume",
                "type": "string",
                "nullable": true,
                "example": "Senior Registered Nurse"
              },
              "experienceSummary": {
                "description": "A summary of the candidate's relevant experience",
                "type": "string",
                "nullable": true,
                "example": "Six years of acute care nursing."
              },
              "explanation": {
                "description": "The structured reasoning behind the score, or null if the stored analysis could not be read as structured data. The score, currentRole and experienceSummary fields are still populated in that case.",
                "type": "object",
                "nullable": true,
                "properties": {
                  "breakdown": {
                    "description": "Per-criterion evaluation behind the overall score",
                    "type": "array",
                    "items": {
                      "type": "object",
                      "description": "The evaluation of a single criterion",
                      "properties": {
                        "category": {
                          "description": "The criterion that was evaluated",
                          "type": "string",
                          "example": "Clinical experience"
                        },
                        "score": {
                          "description": "The score for this criterion, from 0 to 100",
                          "type": "number",
                          "nullable": true,
                          "example": 90
                        },
                        "evidence": {
                          "description": "Evidence from the resume supporting this score",
                          "type": "string",
                          "nullable": true,
                          "example": "Six years in an acute care setting."
                        },
                        "gapAnalysis": {
                          "description": "Gaps identified against this criterion",
                          "type": "string",
                          "nullable": true,
                          "example": "No significant gaps identified."
                        },
                        "transferableSkills": {
                          "description": "Skills that transfer to this criterion from other experience",
                          "type": "string",
                          "nullable": true,
                          "example": "Triage under load."
                        }
                      }
                    }
                  },
                  "keyFitPoints": {
                    "description": "The strongest reasons this candidate fits the position",
                    "type": "array",
                    "items": { "type": "string" },
                    "example": ["Direct ICU experience"]
                  },
                  "areasForDiscussion": {
                    "description": "Topics worth raising with the candidate",
                    "type": "array",
                    "items": { "type": "string" },
                    "example": ["Night shift availability"]
                  }
                }
              }
            }
          },
          "timeCompleted": {
            "description": "The UNIX Epoch time (in seconds) of when the interview was completed",
            "type": "number",
            "example": 1620000000
          },
          "videoURL": {
            "description": "The public URL for an mp4 video recording of this interview, or an empty string if there is no recording",
            "type": "string",
            "example": "https://example.com/video.mp4"
          },
          "pdfURL": {
            "description": "The public URL of the PDF report",
            "type": "string",
            "example": "https://example.com/report.pdf"
          }
        }
      },
      "Interview": {
        "type": "object",
        "properties": {
          "interviewId": {
            "description": "The unique identifier of the interview",
            "type": "string"
          },
          "candidateId": {
            "description": "The unique identifier of the candidate",
            "type": "string"
          },
          "positionId": {
            "description": "The unique identifier of the position",
            "type": "string"
          },
          "questionSummary": {
            "description": "Dictionary mapping each question to a summary of the candidate's answer",
            "type": "object"
          },
          "report": {
            "description": "Dictionary mapping each skill to the candidate's score and feedback",
            "type": "object"
          },
          "timeCompleted": {
            "description": "The UNIX Epoch time (in seconds) of when the interview was completed",
            "type": "number"
          },
          "videoUrl": {
            "description": "The public URL for an mp4 video recording of this interview",
            "type": "string"
          },
          "pdfUrl": {
            "description": "The public URL of the PDF report for this interview",
            "type": "string"
          }
        }
      },
      "Candidate": {
        "type": "object",
        "properties": {
          "candidateId": {
            "description": "The unique identifier of the candidate",
            "type": "string"
          },
          "email": {
            "description": "The email of the candidate",
            "type": "string"
          },
          "firstName": {
            "description": "The first name of the candidate",
            "type": "string"
          },
          "lastName": {
            "description": "The last name of the candidate",
            "type": "string"
          },
          "resumeUrl": {
            "description": "The public URL of the candidate's resume, if it exists",
            "type": "string",
            "nullable": true
          },
          "externalCandidateId": {
            "description": "The ID of this candidate in the connected ATS, or null if there is no linked ATS record",
            "type": "string",
            "nullable": true,
            "example": "67890"
          }
        }
      },
      "Error": {
        "required": ["error", "message"],
        "type": "object",
        "properties": {
          "error": {
            "type": "boolean"
          },
          "message": {
            "type": "string"
          }
        }
      },
      "ApiErrorResponse": {
        "required": ["success", "message"],
        "type": "object",
        "properties": {
          "success": {
            "type": "boolean",
            "example": false
          },
          "message": {
            "type": "string",
            "example": "Missing required fields"
          },
          "code": {
            "type": "string",
            "description": "Machine-readable error code. Lets a retention pipeline distinguish a bad identifier (safe to skip) from an auth failure (must alert).",
            "enum": ["UNAUTHORIZED", "INVALID_REQUEST", "CANDIDATE_NOT_FOUND", "INTERNAL_SERVER_ERROR"],
            "example": "CANDIDATE_NOT_FOUND"
          }
        }
      },
      "InviteCandidateRequest": {
        "type": "object",
        "required": ["firstName", "lastName"],
        "description": "Exactly one of `jobExternalId` or `interviewerId` must be provided. At least one of `email` or `mobile` is required.",
        "properties": {
          "firstName": {
            "type": "string",
            "description": "Candidate's first name",
            "example": "Jane"
          },
          "lastName": {
            "type": "string",
            "description": "Candidate's last name",
            "example": "Doe"
          },
          "email": {
            "type": "string",
            "description": "Candidate's email address. At least one of `email` or `mobile` is required.",
            "example": "jane.doe@example.com"
          },
          "mobile": {
            "type": "string",
            "description": "Candidate's mobile phone number in E.164 format. At least one of `email` or `mobile` is required.",
            "example": "+14155551234"
          },
          "jobExternalId": {
            "type": "string",
            "description": "ATS-side job identifier you supplied when creating the job (matches the `externalJobId` from `/createJob`). Provide either this or `interviewerId`, not both.",
            "example": "REQ-12345"
          },
          "interviewerId": {
            "type": "string",
            "description": "Alex-side job identifier returned from `/createJob` (`payload`) or `/interviewers`. Provide either this or `jobExternalId`, not both.",
            "example": "acme-REQ-12345"
          },
          "candidateExternalId": {
            "type": "string",
            "description": "Optional ATS-side candidate identifier. When provided, repeated calls for the same external id will return the existing candidate instead of creating a duplicate.",
            "example": "CAND-98765"
          }
        }
      },
      "InviteCandidateResponse": {
        "type": "object",
        "required": ["success", "message", "payload"],
        "properties": {
          "success": {
            "type": "boolean",
            "example": true
          },
          "message": {
            "type": "string",
            "example": "Candidate invited"
          },
          "payload": {
            "type": "string",
            "description": "Application id for the new invitation. The Workflows V8 invite trigger creates the session(s) downstream.",
            "example": "app_abc123"
          }
        }
      },
      "StructuredAdditionalQuestion": {
        "type": "object",
        "required": ["question"],
        "properties": {
          "question": {
            "type": "string",
            "description": "The question to ask the candidate",
            "example": "Are you authorized to work in the United States?"
          },
          "answers": {
            "type": "array",
            "description": "Optional list of expected answer choices. When supplied, the interview will treat this as a constrained-answer question.",
            "items": {
              "type": "string"
            },
            "example": ["Yes", "No"]
          },
          "tagName": {
            "type": "string",
            "description": "Optional tag name to attach to the candidate's answer in their report",
            "example": "Work Authorization"
          },
          "tagInstructions": {
            "type": "string",
            "description": "Optional instructions to the evaluator for assigning the tag value",
            "example": "Set to 'Yes' if the candidate confirms US work authorization, otherwise 'No'."
          }
        }
      },
      "CreateJobRequest": {
        "type": "object",
        "required": ["externalJobId", "jobDescription"],
        "properties": {
          "externalJobId": {
            "type": "string",
            "description": "Your ATS-side identifier for the job. Must be unique within your company; subsequent calls with the same value will return a 400.",
            "example": "REQ-12345"
          },
          "jobDescription": {
            "type": "string",
            "description": "Full job description text. Used to generate the interview's questions, skills, and evaluation criteria.",
            "example": "We are looking for a senior backend engineer with experience building distributed systems..."
          },
          "jobTitle": {
            "type": "string",
            "description": "The job's title. When omitted or blank, a title is inferred from `jobDescription`. Supplying it also sharpens the generated questions and evaluation criteria.",
            "example": "Senior Backend Engineer"
          },
          "externalJobTitle": {
            "type": "string",
            "description": "The candidate-facing job title, when it should differ from `jobTitle`. When omitted or blank, it mirrors `jobTitle` (or the inferred title).",
            "example": "Backend Engineer, Platform"
          },
          "questions": {
            "type": "object",
            "description": "Optional questions for each Alex interview round, keyed by round number (`\"1\"`, `\"2\"`, ... up to the number of rounds in your default workflow). A round you supply questions for asks exactly those, in order and as written (surrounding whitespace is trimmed): none are generated, none come from a template, and `intelligentlyOrderQuestions` does not reorder them. Without `criteria` for the same round, its evaluation criteria are generated to grade the answers to these questions. A round you leave out is generated from the job description. Keys must name Alex interview rounds; a human interview round cannot take questions. `null` is read as not sent.",
            "additionalProperties": {
              "type": "array",
              "minItems": 1,
              "items": {
                "type": "string",
                "minLength": 1
              }
            },
            "example": {
              "1": [
                "Do you have the legal right to work in the United Kingdom?",
                "Are you currently enrolled in or have you completed a STEM degree?"
              ],
              "2": ["What is your notice period?", "Briefly describe your most impactful project."]
            }
          },
          "criteria": {
            "type": "object",
            "description": "Optional evaluation criteria for each Alex interview round, keyed by round number like `questions`. A round you supply criteria for is graded on exactly those: none are generated and none come from a template. Without `questions` for the same round, its questions are generated to cover these criteria. Within a round, either every criterion has a `weight` or none does. `null` is read as not sent.",
            "additionalProperties": {
              "type": "array",
              "minItems": 1,
              "items": {
                "$ref": "#/components/schemas/CreateJobCriterion"
              }
            },
            "example": {
              "1": [
                {
                  "name": "Right to work in the UK",
                  "description": "Pass if the candidate confirms they have the legal right to work in the UK.",
                  "weight": 80
                },
                {
                  "name": "STEM degree",
                  "description": "Pass if the candidate is enrolled in or has completed a STEM degree.",
                  "weight": 20
                }
              ],
              "2": [
                {
                  "name": "Impact and ownership",
                  "description": "Clear, specific example of a project they drove and its outcome.",
                  "weight": 100
                }
              ]
            }
          },
          "additionalQuestions": {
            "type": "array",
            "description": "Optional free-text questions to append to the first Alex interview round, after its generated or supplied questions.",
            "items": {
              "type": "string"
            },
            "example": ["What is your expected start date?"]
          },
          "structuredAdditionalQuestions": {
            "type": "array",
            "description": "Optional structured questions with answer choices and/or tagging instructions, appended to the first Alex interview round. Use this instead of `additionalQuestions` when you need constrained-answer questions or report tags.",
            "items": {
              "$ref": "#/components/schemas/StructuredAdditionalQuestion"
            }
          },
          "intelligentlyOrderQuestions": {
            "type": "boolean",
            "description": "When `true`, the first Alex interview round's questions, including `additionalQuestions` and `structuredAdditionalQuestions`, are reordered to flow naturally. Ignored when you supply that round's `questions`: those are asked exactly in the order you wrote them, with additional questions after them. Defaults to `false`.",
            "example": false
          },
          "additionalGenerationContext": {
            "type": "string",
            "description": "Optional free-text guidance to bias question generation (e.g. seniority signals, must-have skills).",
            "example": "Emphasize systems-design experience over framework specifics."
          },
          "active": {
            "type": "boolean",
            "description": "Whether the job is created active (`true`) or inactive (`false`). When omitted, the company's default job status is used.",
            "example": true
          },
          "templateId": {
            "type": "string",
            "description": "Interview guide template to apply instead of the company's default. Must belong to your company (see `GET /templates`). When omitted, the company default is used if one is configured.",
            "example": "clx8f2k1p0000abcd1234efgh"
          }
        }
      },
      "CreateJobCriterion": {
        "type": "object",
        "required": ["name", "description"],
        "properties": {
          "name": {
            "type": "string",
            "minLength": 1,
            "description": "The criterion's name, trimmed. Unique within its round, ignoring case.",
            "example": "Right to work in the UK"
          },
          "description": {
            "type": "string",
            "minLength": 1,
            "description": "How to grade the candidate against this criterion: what passing or a strong answer looks like. Must not be blank; trimmed.",
            "example": "Pass if the candidate confirms they have the legal right to work in the UK."
          },
          "weight": {
            "type": "number",
            "exclusiveMinimum": 0,
            "maximum": 100,
            "description": "The criterion's relative share of the round's score. A round's score divides each weight by the sum of its round's weights, so weights need not add up to 100: `80`/`20` grades the same as `0.8`/`0.2` or `4`/`1`. Stored to two decimal places. Omit it (or send `null`) on every criterion of a round to weigh them equally.",
            "example": 80
          }
        }
      },
      "CreateJobResponse": {
        "type": "object",
        "required": ["success", "message", "payload"],
        "properties": {
          "success": {
            "type": "boolean",
            "example": true
          },
          "message": {
            "type": "string",
            "example": "Job generation initiated."
          },
          "payload": {
            "type": "string",
            "description": "The deterministic Alex `interviewerId` the job will be assigned once generation completes. Built from your company slug and `externalJobId`. Use this with `/inviteCandidate` (as `interviewerId`) or poll `/interviewers` to detect when the job is ready.",
            "example": "acme-REQ-12345"
          }
        }
      },
      "CreateTemplateQuestion": {
        "type": "object",
        "required": ["content"],
        "properties": {
          "content": {
            "type": "string",
            "description": "The question text",
            "example": "Tell me about a time you owned a production incident."
          },
          "interviewInstructions": {
            "type": "string",
            "description": "Optional interviewer-facing instructions for this question",
            "example": "Probe for ownership, not just participation."
          }
        }
      },
      "CreateTemplateCriterion": {
        "type": "object",
        "required": ["name"],
        "properties": {
          "name": {
            "type": "string",
            "description": "Criterion name",
            "example": "Communication"
          },
          "evaluationInstructions": {
            "type": "string",
            "description": "How the criterion should be scored",
            "example": "Look for clear, structured answers."
          },
          "priority": {
            "type": "string",
            "description": "Priority label, e.g. Low, Medium, or High",
            "example": "High"
          }
        }
      },
      "CreateTemplateTag": {
        "type": "object",
        "required": ["name"],
        "properties": {
          "name": {
            "type": "string",
            "example": "Work Authorization"
          },
          "outputType": {
            "type": "string",
            "description": "Tag value type. Defaults to `string`.",
            "example": "string"
          },
          "outputOptions": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Allowed values when the tag is constrained"
          },
          "instructions": {
            "type": "string",
            "description": "How the evaluator should assign the tag",
            "example": "Set to Yes if the candidate confirms US work authorization."
          }
        }
      },
      "CreateTemplateRequest": {
        "type": "object",
        "required": ["name"],
        "properties": {
          "name": {
            "type": "string",
            "description": "Template name. Must be unique within your company.",
            "example": "SWE Screen"
          },
          "intro": {
            "type": "string",
            "description": "Opening script for the interview"
          },
          "outro": {
            "type": "string",
            "description": "Closing script for the interview"
          },
          "interviewCreationNotes": {
            "type": "string",
            "description": "Notes that guide question generation when this template is applied to a job"
          },
          "additionalEvaluationInstructions": {
            "type": "string"
          },
          "additionalInterviewInstructions": {
            "type": "string"
          },
          "estimatedDuration": {
            "type": "integer",
            "description": "Estimated interview duration in minutes",
            "example": 25
          },
          "questionLimit": {
            "type": "integer",
            "description": "Cap on generated questions. `0` adds none beyond the template questions."
          },
          "language": {
            "type": "string",
            "example": "en"
          },
          "questions": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/CreateTemplateQuestion"
            }
          },
          "criteria": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/CreateTemplateCriterion"
            }
          },
          "tags": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/CreateTemplateTag"
            }
          }
        }
      },
      "CreateTemplateResponse": {
        "type": "object",
        "required": ["success", "message", "payload"],
        "properties": {
          "success": {
            "type": "boolean",
            "example": true
          },
          "message": {
            "type": "string",
            "example": "Template created successfully"
          },
          "payload": {
            "type": "string",
            "description": "The created template id. Pass this as `templateId` on `/createJob`.",
            "example": "clx8f2k1p0000abcd1234efgh"
          }
        }
      },
      "DeleteCandidateRequest": {
        "type": "object",
        "description": "Supply exactly one identifier.",
        "properties": {
          "candidateId": {
            "type": "string",
            "description": "The Alex-side candidate ID, as returned by `/candidates`.",
            "example": "clx8f2k1p0000abcd1234efgh"
          },
          "candidateExternalId": {
            "type": "string",
            "description": "The ATS-side candidate ID you supplied to `/inviteCandidate`.",
            "example": "ATS-4471"
          },
          "email": {
            "type": "string",
            "description": "The candidate's email address. Matched case-insensitively within your company.",
            "example": "candidate@example.com"
          }
        }
      },
      "DeletedCandidateRecord": {
        "type": "object",
        "description": "Snapshot of a candidate captured immediately before deletion.",
        "properties": {
          "candidateId": {
            "type": "string",
            "example": "clx8f2k1p0000abcd1234efgh"
          },
          "candidateExternalId": {
            "type": "string",
            "nullable": true,
            "example": "ATS-4471"
          },
          "email": {
            "type": "string",
            "example": "candidate@example.com"
          },
          "firstName": {
            "type": "string",
            "example": "Jordan"
          },
          "lastName": {
            "type": "string",
            "example": "Reyes"
          },
          "phoneNumber": {
            "type": "string",
            "nullable": true,
            "example": "+15551234567"
          },
          "createdAt": {
            "type": "string",
            "example": "2026-03-14T10:02:11.000Z",
            "format": "date-time",
            "nullable": true,
            "description": "ISO 8601. Null if the stored value could not be parsed."
          },
          "sessionIds": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Interview sessions removed alongside the candidate.",
            "example": ["ses_9f2b1c"]
          },
          "applicationIds": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Applications removed alongside the candidate.",
            "example": ["app_4d71ae"]
          }
        }
      },
      "DeleteCandidateResponse": {
        "type": "object",
        "required": ["success", "code", "message", "deletedCount", "data"],
        "properties": {
          "success": {
            "type": "boolean",
            "example": true
          },
          "code": {
            "type": "string",
            "example": "DELETED"
          },
          "message": {
            "type": "string",
            "example": "Deleted 1 candidate record(s)."
          },
          "deletedCount": {
            "type": "integer",
            "description": "Number of candidate records deleted. An `email` lookup ignores case, so it can match more than one record; `candidateId` and `candidateExternalId` match at most one.",
            "example": 1
          },
          "data": {
            "type": "object",
            "properties": {
              "candidates": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/DeletedCandidateRecord"
                }
              },
              "assets": {
                "type": "object",
                "properties": {
                  "resumesDeleted": {
                    "type": "integer",
                    "description": "Resume files queued for deletion.",
                    "example": 1
                  },
                  "sessionsCleaned": {
                    "type": "integer",
                    "description": "Sessions whose recordings, reports, and uploads were queued for deletion from storage.",
                    "example": 3
                  },
                  "cleanupSucceeded": {
                    "type": "boolean",
                    "description": "True once the background deletion of stored files has started; false if it could not be started. The database records are deleted either way.",
                    "example": true
                  }
                }
              }
            }
          }
        }
      }
    },
    "securitySchemes": {
      "bearerAuth": {
        "type": "http",
        "scheme": "bearer"
      },
      "apiKeyAuth": {
        "type": "apiKey",
        "in": "header",
        "name": "X-API-Key"
      }
    }
  }
}
