Skip to content

API Reference

Base URL: VITE_SERVER_URL/v1

All protected endpoints require: Authorization: Bearer <Firebase idToken>


Stage Types

GET /v1/stage-types

Returns all active stage types. No auth required. No plan filtering — all 6 types returned on every plan.

Response: IStageTypeConfig[] sorted by order


Jobs

GET /v1/jobs

List jobs for the authenticated recruiter's organization.

Query params: status, page, limit

GET /v1/jobs/:id

Job detail with stages[] populated (question refs not populated).

POST /v1/jobs

Create a job.

Body:

typescript
{
  title: string;
  description: string;
  department?: string;
  location?: string;
  employmentType?: 'full_time' | 'part_time' | 'contract' | 'internship';
  stages: {
    name: string;
    stageTypeKey: StageTypeKey;
    screeningConfig?: { questions: { text: string; order: number }[]; ... };
    dsaConfig?: { problems: { problemId: string; order: number }[]; ... };
    live1on1Config?: { privateQuestions: { questionId: string; order: number }[]; ... };
    // ...
  }[];
}

Errors:

  • 400 — Zod validation failure
  • 403maxActiveJobs limit reached

PATCH /v1/jobs/:id

Update job. (Note: syncActivePipelines() — syncing active pipeline stage names on job edit — is deferred and not yet implemented.)

DELETE /v1/jobs/:id

Soft-delete. Sets isDeleted: true, status: 'archived'.


Interviews

POST /v1/interviews

Schedule an interview (invite a candidate to a stage).

Two request formats depending on stage type:

Format A — pipeline-aware automated screening (stageId + isAutomated: true or omitted):

typescript
{
  jobOpeningId: string;
  stageId: string;
  stageIndex: number;
  participant: { email: string; name?: string };
  candidatePipelineId?: string;
  clientUrl: string;
  sendAutomatedLink?: boolean;
}

Format B — pipeline-aware live interview (stageId + isAutomated: false):

typescript
{
  jobOpeningId: string;
  title: string;
  round: string;
  startTime: string;   // ISO datetime, required for live
  endTime: string;     // ISO datetime, required for live
  baseUrl: string;
  clientUrl: string;
  participant: { email: string; name?: string };
  isAutomated: false;
  stageId: string;
  stageIndex: number;
  candidatePipelineId?: string;
  recruiterName?: string;
  recruiterEmail?: string;
}

Format C — legacy live interview (no stageId): same as B without stageId/stageIndex/candidatePipelineId.

Notes:

  • Format B stores candidatePipelineId + stageId on Interview, sets meetingLink = ${baseUrl}/room/${interview._id}, calls markStageInvited()
  • Format C uses ${baseUrl}/lobby/${nanoid(10)} for meetingLink (no pipeline awareness)

Errors:

  • 403 — usage limit exceeded (maxInterviewsPerMonth)
  • 409 — stage status guard: stage is pending (locked, must unlock first), OR already invited/in_progress/completed, OR an active interview for (candidatePipelineId, stageId) already exists

GET /v1/interviews/:id

Interview detail (recruiter view — all fields including internal). Returns feedbacks[] and candidateFeedback.

POST /v1/interviews/:id/feedback

Submit recruiter feedback. Auth required. Required before unlock-stage for live_1on1 and culture_fit_hr stages.

Body:

typescript
{
  interviewerEmail: string;
  overallRating: number;           // 1–10 (integer)
  traits: string[];                // selected trait chips
  recommendation: 'strong_yes' | 'yes' | 'no' | 'strong_no';
  comments: string;                // min 5 chars
}

Side effects:

  • Sets interview.status = 'completed', captures endTime
  • If candidatePipelineId + stageId present: calls markStageCompleted() with result derived from recommendation (strong_yes | yes → 'pass', else 'hold'), candidateStatus = 'completed'

POST /v1/interviews/:id/candidate-feedback

Submit candidate's post-call feedback. Auth required. Does NOT affect pipeline or interview status.

Body:

typescript
{
  overallRating: number;           // 1–10 (integer)
  traits: string[];
  recommendation: 'strong_yes' | 'yes' | 'no' | 'strong_no';
  comments?: string;               // optional
}

Stored in interview.candidateFeedback (single sub-doc, not in feedbacks[]).

GET /v1/interviews/:id/private-questions (Phase 1C)

Returns private question list for live session. Auth: recruiter must be in interview.interviewers.

POST /v1/interviews/decline/:declineToken

No auth required. Registered before /:id routes to prevent param collision.

Body (optional): { reason?: string, tags?: string[] }

Finds interview by declineToken (no status filter). Saves stageData.declineData, marks interview declined, updates pipeline, and queues recruiter notification email.


Screening Sessions (No Auth)

GET /v1/screening/:token

Validate token, return session config + questions (no AI scores, no internal fields).

POST /v1/screening/:token/start

Mark interview in_progress, update pipeline stage status.

POST /v1/screening/:token/submit

Submit responses. Triggers async AI evaluation.


Pipeline

GET /v1/pipeline

Query: ?jobId=:id — List all pipelines for a job.

GET /v1/pipeline/:id

Full pipeline detail. Populates all stageProgression[].interviewId refs.

PATCH /v1/pipeline/:id/status

Set global pipeline status.

Body: { status: 'shortlisted' | 'rejected' | 'hired' | 'active' | 'withdrawn' }

POST /v1/pipeline/:id/unlock-stage

Unlock the next stage. Supports a force flag to cascade-complete preceding incomplete stages.

Body: { stageIndex: number, force?: boolean }

Errors:

  • 400 requiresFeedback: true — a preceding live_1on1 or culture_fit_hr stage exists without recruiter feedback. force: true cannot bypass this guard. Response includes pendingStages: string[].
  • 409 requiresForce: true — preceding stages not yet completed. Use force: true to cascade-complete them (frontend shows confirmation dialog first). Note: this guard is skipped if the requiresFeedback guard fires first.

POST /v1/pipeline/:id/notes

Add internal recruiter note.

GET /v1/pipeline/pool

Aggregated list of unique candidates across all jobs for the org.

Registered before /:id to avoid param collision.

Query params:

ParamTypeDefaultNotes
searchstringRegex match on name + email (min 2 chars)
statusenumactive | shortlisted | rejected | withdrawn | hired
sortenumlastActivitylastActivity | name | jobCount
pagenumber1
limitnumber20max 100

Response:

json
{
  "success": true,
  "data": {
    "candidates": [
      {
        "participantId": "...",
        "name": "Jane Smith",
        "email": "jane@example.com",
        "jobCount": 3,
        "latestStatus": "active",
        "lastActivityAt": "2026-03-01T..."
      }
    ],
    "pagination": { "total": 42, "page": 1, "limit": 20, "totalPages": 3 },
    "summary": { "total": 42, "active": 28, "hired": 5, "shortlisted": 9 }
  }
}

Aggregation stages: $match (org)$sort (lastActivityAt)$lookup (Participant)$unwind → optional search $match$group by participantId$facet { data, total, summary }.

The summary facet runs before the status filter, so stat card counts always reflect search-filtered unique candidates regardless of the status dropdown.

GET /v1/pipeline/pool/:participantId

All pipelines for a single participant within the org, with participant detail.

Registered before /:id.

Response:

json
{
  "success": true,
  "data": {
    "participant": { "_id": "...", "name": "Jane", "email": "jane@example.com", "stats": { ... } },
    "pipelines": [ /* full CandidatePipeline docs, sorted by lastActivityAt desc */ ]
  }
}

Candidate Dashboard (/v1/candidate/*)

All require candidate Firebase JWT.

GET /v1/candidate/dashboard

All pipelines for the authenticated candidate, sorted by lastActivityAt desc. Returns candidateFacingStatus and stageProgression[].candidateStatus only (projected).

GET /v1/candidate/pipeline/:id

Full pipeline detail — projected. Verifies pipeline.participantId matches req.participant._id.

GET /v1/candidate/interviews/:id

Past interview session — projected. Own answers only; no AI scores, no feedback.

PATCH /v1/candidate/interviews/:id/rsvp

Accept or decline a live interview invite.

Body: { status: 'accepted' | 'declined' }

PATCH /v1/candidate/preferences

Update candidate notification preferences.


Question Banks

GET /v1/screening-questions

Query: ?source=system&category=...

POST /v1/screening-questions

Create a custom question for the org.

GET /v1/interview-questions (Phase 1C)

Query: ?questionType=behavioral&applicableStageTypes=live_1on1

POST /v1/interview-questions (Phase 1C)

GET /v1/dsa-problems (Phase 3)

Query: ?difficulty=medium&tags=array

POST /v1/dsa-problems (Phase 3)

GET /v1/dsa-problems/:id (Phase 3)

Returns problem without hidden test cases.

GET /v1/scenario-questions (Phase 4)

Query: ?applicableStageTypes=ai_conversational&scenarioType=behavioral

POST /v1/scenario-questions (Phase 4)


DSA Sessions (Phase 3, No Auth)

GET /v1/dsa/:token

Load problem list, starter code, time limits.

POST /v1/dsa/:token/run

Run code against visible test cases.

POST /v1/dsa/:token/submit

Final submission — run against hidden test cases, store results.


Organization Management (Phase 5)

GET /v1/orgs/:id

PATCH /v1/orgs/:id

GET /v1/orgs/:id/members

POST /v1/orgs/:id/members/invite

PATCH /v1/orgs/:id/members/:userId

DELETE /v1/orgs/:id/members/:userId

GET /v1/orgs/:id/features

POST /v1/orgs/:id/plan

GET /v1/plans (public, no auth)


Analytics (Phase 6, gated by advancedAnalytics)

GET /v1/analytics/pipeline-funnel?jobId=:id

GET /v1/analytics/time-to-hire?jobId=:id

GET /v1/analytics/question-effectiveness

GET /v1/analytics/interviewer-activity


Webhooks

POST /v1/webhooks/stripe (Phase 5)

Handles customer.subscription.updated and customer.subscription.deleted.


Health

GET /health

Returns { status: 'ok', db: 'connected', queue: { depth: n } } (Phase 6 adds queue depth)