Evaluation Service API

The Evaluation Service drives the persona questionnaire flow and runs the matching algorithm once all questions are answered. It runs on internal port 9103 and is exposed through the gateway at the /evaluation prefix.

Base paths:

Environment URL
Production https://api.zeswa.com/evaluation
Local http://localhost:9103

All endpoints require a valid Bearer token issued by the Profile Service (POST /persona/login).


Overview

The evaluation lifecycle is:

  1. Admin creates (or resumes) an evaluationPOST /evaluation with a personaId and teamId. If an active evaluation already exists for that persona + team, it is returned instead of creating a new one (resumed: true). Otherwise a new one is created.
  2. User progressesGET /evaluation/:id returns the next unanswered question. The user submits each answer via POST /evaluation/:id/answer, which returns the next question. Clients that need a structural view of the entire questionnaire (e.g. a sidebar / progress rail) can call GET /evaluation/:id/questions for the full ordered list with per-question status.
  3. Resume after abort — if the user abandons the session, answers are preserved. Calling POST /evaluation again returns the same evaluation with resumed: true and the next unanswered question.
  4. Completion — once all questions are answered, the algorithm runs automatically and the results are stored. Any subsequent GET /evaluation/:id returns the full match results.

GET /evaluation

Lists evaluations for a persona, optionally filtered by team or status. Use this to discover an active evaluation without knowing its ID.

Authentication: Required.

Query parameters

Parameter Type Required Description
personaId string UUID of the persona
teamId integer Filter by team ID
status string Filter by status: pending, in_progress, or complete

Success response — 200 OK

{
  "items": [
    {
      "id": "a1b2c3d4-...",
      "personaId": "3f4e2d1c-...",
      "teamId": 1,
      "status": "in_progress",
      "progress": { "answered": 42, "total": 158 },
      "createdAt": "2024-01-15T10:30:00.000Z",
      "updatedAt": "2024-01-15T11:00:00.000Z"
    }
  ],
  "total": 1
}

Error responses

Status Condition
400 personaId query param missing
401 Missing Authorization header
403 Invalid or expired token
500 Internal server error

POST /evaluation

Creates a new evaluation session, or resumes an existing active one.

If a pending or in_progress evaluation already exists for the same personaId + teamId, it is returned with resumed: true and currentQuestion pointing to the next unanswered question. No duplicate evaluation is created.

Authentication: Required (admin token).

Request body

{
  "personaId": "3f4e2d1c-...",
  "teamId": 1
}
Field Type Required Description
personaId string The ID of the persona being evaluated
teamId integer The numeric ID of the algorithm team to match against

Success response — 201 Created (new evaluation)

{
  "id": "a1b2c3d4-...",
  "personaId": "3f4e2d1c-...",
  "teamId": 1,
  "status": "pending",
  "resumed": false,
  "progress": {
    "answered": 0,
    "total": 158
  },
  "currentQuestion": {
    "id": 1,
    "text": "I enjoy working in a structured environment",
    "type": "LIKERT",
    "likertAnswerSet": "agree",
    "questionGroupId": 1,
    "questionGroupName": "Work Style",
    "questionGroupTitle": "Your Work Style"
  },
  "createdAt": "2024-01-15T10:30:00.000Z",
  "updatedAt": "2024-01-15T10:30:00.000Z"
}

Success response — 200 OK (resumed evaluation)

{
  "id": "a1b2c3d4-...",
  "personaId": "3f4e2d1c-...",
  "teamId": 1,
  "status": "in_progress",
  "resumed": true,
  "progress": {
    "answered": 6,
    "total": 158
  },
  "currentQuestion": {
    "id": 7,
    "text": "I rarely work outside of a 9 to 5 workday",
    "type": "LIKERT",
    "likertAnswerSet": "agree",
    "questionGroupId": 4,
    "questionGroupName": "workexpectation",
    "questionGroupTitle": "Work Expectation"
  },
  "createdAt": "2024-01-15T10:30:00.000Z",
  "updatedAt": "2024-01-15T10:45:00.000Z"
}

Error responses

Status Condition
401 Missing Authorization header
403 Invalid or expired token
404 teamId does not exist in the database
400 Validation failed (missing/invalid fields)
500 Internal server error

GET /evaluation/:id

Returns the current state of an evaluation. If the evaluation is incomplete, the response includes the next unanswered question. If the evaluation is complete, the response includes the algorithm match results.

Authentication: Required.

Path parameters

Parameter Type Description
id string Evaluation UUID

Success response — 200 OK (incomplete)

{
  "id": "a1b2c3d4-...",
  "personaId": "3f4e2d1c-...",
  "teamId": 1,
  "status": "in_progress",
  "progress": {
    "answered": 12,
    "total": 158
  },
  "currentQuestion": {
    "id": 15,
    "text": "I prefer to resolve conflicts through compromise",
    "type": "LIKERT",
    "likertAnswerSet": "agree",
    "questionGroupId": 3,
    "questionGroupName": "Conflict Resolution",
    "questionGroupTitle": "How You Handle Conflict"
  },
  "createdAt": "2024-01-15T10:30:00.000Z",
  "updatedAt": "2024-01-15T10:35:00.000Z"
}

Success response — 200 OK (complete)

{
  "id": "a1b2c3d4-...",
  "personaId": "3f4e2d1c-...",
  "teamId": 1,
  "status": "complete",
  "progress": {
    "answered": 158,
    "total": 158
  },
  "results": {
    "overallScore": 72.5,
    "overallCategory": "high",
    "totalTimeTaken": 840,
    "dimensions": [
      {
        "questionGroupId": 1,
        "name": "Work Style",
        "scoreType": "SUPPLEMENTARY",
        "scorePercentage": 78.3,
        "scoreCategory": "high",
        "weightPercentage": 15
      }
    ]
  },
  "createdAt": "2024-01-15T10:30:00.000Z",
  "updatedAt": "2024-01-15T11:00:00.000Z"
}

Score categories

Category Score range Meaning
high ≥ 66% Strong match
medium 33% – 65% Moderate match
low < 33% Weak match

Error responses

Status Condition
401 Missing Authorization header
403 Invalid or expired token
404 Evaluation not found
500 Internal server error

POST /evaluation/:id/answer

Submits an answer for a question. Returns the next unanswered question, or the final results if all questions have been answered.

Authentication: Required.

Path parameters

Parameter Type Description
id string Evaluation UUID

Request body

{
  "questionId": 15,
  "value": 4,
  "timeTaken": 12
}
Field Type Required Description
questionId integer The ID of the question being answered
value integer (1–5) The Likert scale answer (1 = strongly disagree, 5 = strongly agree)
timeTaken integer (≥ 0) Seconds the user spent on this question. Defaults to 0 if omitted.

Success response — 200 OK (more questions remain)

{
  "id": "a1b2c3d4-...",
  "personaId": "3f4e2d1c-...",
  "teamId": 1,
  "status": "in_progress",
  "progress": {
    "answered": 13,
    "total": 158
  },
  "nextQuestion": {
    "id": 16,
    "text": "I adapt my approach when a strategy is not working",
    "summary": "Assesses behavioural flexibility — whether the candidate adjusts tactics when current approaches fail.",
    "type": "LIKERT",
    "likertAnswerSet": "usually",
    "questionGroupId": 3,
    "questionGroupName": "Conflict Resolution",
    "questionGroupTitle": "How You Handle Conflict"
  }
}

Success response — 200 OK (evaluation complete)

{
  "id": "a1b2c3d4-...",
  "personaId": "3f4e2d1c-...",
  "teamId": 1,
  "status": "complete",
  "progress": {
    "answered": 158,
    "total": 158
  },
  "nextQuestion": null,
  "results": {
    "overallScore": 72.5,
    "overallCategory": "high",
    "totalTimeTaken": 840,
    "dimensions": [ ... ]
  }
}

Error responses

Status Condition
400 value out of range (1–5) or questionId not in this evaluation
401 Missing Authorization header
403 Invalid or expired token
404 Evaluation not found
409 Evaluation is already complete
500 Internal server error

GET /evaluation/:id/questions

Returns the full ordered list of questions for an evaluation, each annotated with its status (answered, current, or pending) and — when answered — the stored answer. Designed for HUB to render a sidebar / progress rail.

Server-side ordering matches the order questions are served by GET /evaluation/:id and POST /evaluation/:id/answer. Exactly one item has status: "current" while the evaluation is active; zero items have status: "current" once the evaluation is complete.

Always returned with Cache-Control: no-store — the response mutates with each answer.

Authentication: Required.

Path parameters

Parameter Type Description
id string Evaluation UUID

Success response — 200 OK

{
  "items": [
    {
      "id": 12,
      "text": "I prefer to plan ahead.",
      "summary": "Planning preference",
      "type": "LIKERT",
      "likertAnswerSet": "agree",
      "questionGroupId": 3,
      "questionGroupName": "Conscientiousness",
      "questionGroupTitle": "How you organise work",
      "order": 0,
      "status": "answered",
      "answer": {
        "value": 4,
        "timeTaken": 12,
        "answeredAt": "2026-05-10T13:42:11.823Z"
      }
    },
    {
      "id": 13,
      "text": "I adapt my approach when a strategy is not working.",
      "type": "LIKERT",
      "likertAnswerSet": "usually",
      "questionGroupId": 3,
      "questionGroupName": "Conscientiousness",
      "questionGroupTitle": "How you organise work",
      "order": 1,
      "status": "current"
    },
    {
      "id": 14,
      "text": "I keep my workspace organised.",
      "type": "LIKERT",
      "likertAnswerSet": "agree",
      "questionGroupId": 3,
      "questionGroupName": "Conscientiousness",
      "questionGroupTitle": "How you organise work",
      "order": 2,
      "status": "pending"
    }
  ],
  "progress": {
    "answered": 1,
    "total": 80
  }
}

Field reference

Field Type Description
items[] array Questions in delivery order. Length equals progress.total under normal operation.
items[].id integer Numeric question ID.
items[].text string Question text shown to the user.
items[].summary string | null Optional short summary of what the question measures.
items[].type string Question type, e.g. LIKERT.
items[].likertAnswerSet string | null Likert answer set key when type === "LIKERT".
items[].questionGroupId integer Numeric ID of the dimension/group this question belongs to.
items[].questionGroupName string Internal group name.
items[].questionGroupTitle string User-facing group title.
items[].order integer 0-based position in the evaluation’s ordered list. Strictly increasing.
items[].status enum answered | current | pending. See semantics below.
items[].answer object | absent Present only when status === "answered". Contains value (1–5), timeTaken, answeredAt.
progress.answered integer Number of answered questions.
progress.total integer Total questions in the evaluation.

Status semantics

Status Meaning answer field
answered User has submitted an answer for this question. Present
current The next unanswered question — same id as currentQuestion.id from GET /:id. Exactly 0 or 1 items carry this status (1 while active, 0 once complete). Absent
pending Not yet answered. Always comes after the current item in order. Absent

Under normal flow the rail reads contiguously: answered…* current pending…*.

Privacy note

The answer.value field is included so HUB can offer a “review my answers” experience if desired. The progress sidebar itself only needs status. If your UI deliberately avoids spoiling past answers, simply do not render item.answer.value.

Error responses

Status Condition
400 Invalid id
401 Missing Authorization header
403 Invalid or expired token
404 Evaluation not found
500 Internal server error

Evaluation Status States

Status Description
pending Created, no answers yet
in_progress At least one answer submitted
complete All questions answered, results available

Testing with the API Runner

A script is provided to simulate the full flow end-to-end against the running service:

# Start the evaluation service first
npm run evaluation

# In another terminal, run the simulation
npm run evaluation:runner

# Override defaults
PERSONA_EMAIL=admin@example.com \
TEAM_ID=1 \
DEFAULT_ANSWER=3 \
npm run evaluation:runner

The runner will:

  1. Obtain an access token via POST /persona/login
  2. Resolve the personaId via GET /persona/me
  3. Create an evaluation
  4. Loop through all questions, submitting value 3 (neutral) for each
  5. Print a formatted results table

POST /evaluation/:id/team-match

Computes a combined Role + Workstyle score for a candidate’s evaluation against a team. On-demand — nothing is persisted.

Authentication: Required.

Request body (all optional)

Field Type Default Description
roleId number First role linked to the evaluation’s team Override the role used for role match.
algorithmUserId number Bridged from the evaluation’s persona Numeric algorithm user id used to look up UserExperience and UserGoal records.
roleWeight number (0…100) 50 Blend weight for role match vs workstyle.

Sample response

{
  "evaluationId": "abc-123",
  "algorithmTeamId": 1,
  "roleId": 1,
  "algorithmUserId": 4,
  "workstyle": { "score": 72.5, "scoreCategory": "high", "dimensions": [/* 16 */] },
  "roleMatch": {
    "roleId": 1, "score": 64.2, "scoreCategory": "medium",
    "experienceScore": 66.0, "goalScore": 30.0,
    "breakdown": [{ "name": "industry", "priority": 1, "percentageMatch": 0.5, "points": 6.5, "maxPoints": 13, "source": "experience" }]
  },
  "combined": { "score": 68.3, "scoreCategory": "high", "roleWeight": 50 }
}

If no Role / RoleSpecification is configured for the team, roleMatch is null and combined.score === workstyle.score.


POST /evaluation/:id/compare-team

Re-scores an existing evaluation against a different team than the one it was created for. The candidate’s stored answers are reused as-is; only the team context (members + their answers) changes. Read-only and on-demand — nothing is persisted.

Use case: Candidate completed an evaluation in Team A; admin wants to see how compatible they would be with Team B without re-collecting answers or creating a new evaluation.

Authentication: Required.

Path parameters

Parameter Type Description
id string Source evaluation UUID (the one created against Team A).

Request body

Field Type Required Default Description
algorithmTeamId integer (≥ 1) Numeric id of the target team to compare against. Must differ from the evaluation’s own algorithmTeamId.
roleId integer (≥ 1) Optional role for an additional role match score (typically a role on the target team).
algorithmUserId integer (≥ 0) Bridged from persona Numeric algorithm user id used for role match UserExperience/UserGoal lookups.
roleWeight number (0…100) 50 Blend weight for combined Role + Workstyle. Only used if a role match is computed.

Sample response — 200 OK

{
  "evaluationId": "abc-123",
  "personaId": "3f4e2d1c-...",
  "sourceTeam": { "algorithmTeamId": 1, "teamName": "Engineering Core" },
  "targetTeam": { "algorithmTeamId": 2, "teamName": "Growth Squad" },
  "workstyle": {
    "overallScore": 67.8,
    "overallCategory": "high",
    "dimensions": [
      { "questionGroupId": 1, "name": "values", "scoreType": "TOP_TEN",
        "scorePercentage": 72.4, "scoreCategory": "high", "weightPercentage": 100 }
    ]
  },
  "roleMatch": null,
  "combined": null,
  "answersUsed": 158,
  "computedAt": "2026-05-10T15:14:32.765Z"
}

When roleId is supplied and a RoleSpecification resolves, roleMatch and combined are populated:

{
  "roleMatch": {
    "roleId": 4, "score": 65.0, "scoreCategory": "medium",
    "experienceScore": 70, "goalScore": 60,
    "breakdown": [/* … */]
  },
  "combined": { "score": 66.4, "scoreCategory": "medium", "roleWeight": 50 }
}

If a roleId was provided but the role match couldn’t be computed, roleMatch is returned with an error field instead of a score, and combined remains null.

Headers

  • Cache-Control: no-store — per-call, deterministic but not cacheable.

Error responses

Status Condition
400 algorithmTeamId missing/non-positive
400 Evaluation has no answers yet (answers.length === 0)
400 algorithmTeamId equals the evaluation’s own team — use GET /:id (own results) or POST /:id/team-match instead
401 Missing Authorization header
403 Invalid or expired token
404 Evaluation not found
404 Target team algorithmTeamId not found
500 Internal server error

See the Compare Team — Admin Portal Integration Guide for the recommended UX and TypeScript fetcher.


POST /evaluation/compare

Computes pairwise compatibility between two completed evaluations.

Authentication: Required.

Request body

Field Type Required Description
evaluationIdA string First evaluation UUID.
evaluationIdB string Second evaluation UUID (must differ from A).
mode enum PAIRWISE (default) / MUTUAL / SIMULATED_TEAM

Modes

  • PAIRWISE — A is the team-of-one; B is the candidate. Asymmetric.
  • MUTUAL — Run PAIRWISE in both directions and average. Symmetric. Returns reverseScore and reverseDimensions so the UI can show both raw scores.
  • SIMULATED_TEAM — same calculation as MUTUAL; conceptually framed as 2-person team cohesion.

Sample response

{
  "mode": "MUTUAL",
  "evaluationIdA": "abc-123",
  "evaluationIdB": "def-456",
  "score": 71.4,
  "scoreCategory": "high",
  "reverseScore": 68.9,
  "dimensions": [/* … */],
  "reverseDimensions": [/* … */]
}

See Algorithm: Role Match & Combined Scoring for the full math, and the Admin and HUB UI integration guides for screen-by-screen wiring.