Skip to main content

Recruitment API

The hiring chain, each stage a module:

staffing-plans ─▶ job-requisitions ─▶ job-openings ─▶ job-applicants ─▶ job-offers ─▶ (employee + user created)
▲ │
public careers pages interview-schedules + feedback

Functional spec: ../modules/recruitment.md. Conventions and Swagger: conventions.md, README.md.

Permissions used here: hr.recruitment.view (read anything), hr.recruitment.manage (requisitions/openings/applicants), hr.recruitment.interview.manage, hr.recruitment.offer.manage, hr.recruitment.staffing.manage.

Job requisitions — /api/job-requisitions

JobRequisitionController.java

MethodPathPermissionPurpose
GET/api/job-requisitionshr.recruitment.viewPaged/filtered list
GET/api/job-requisitions/{id}hr.recruitment.viewOne requisition
POST/api/job-requisitionshr.recruitment.manageCreate
PUT/api/job-requisitions/{id}hr.recruitment.manageUpdate
PATCH/api/job-requisitions/{id}/statushr.recruitment.manageMove through the approval workflow
PATCH/api/job-requisitions/{id}/archive · /restorehr.recruitment.manageSoft-delete / restore
DELETE/api/job-requisitions/{id}hr.recruitment.manageDelete

Job openings — /api/job-openings

JobOpeningController.java

MethodPathPermissionPurpose
GET/api/job-openings · /{id}hr.recruitment.viewPaged list / one opening
POST · PUT /{id}/api/job-openings...hr.recruitment.manageCreate / update
PATCH/api/job-openings/{id}/statushr.recruitment.manageOpen / close the posting
DELETE/api/job-openings/{id}hr.recruitment.manageDelete

Openings with status Open are what the public careers endpoints expose.

Job applicants — /api/job-applicants

JobApplicantController.java

MethodPathPermissionPurpose
GET/api/job-applicantshr.recruitment.viewPaged/filtered list
GET/api/job-applicants/pipelinehr.recruitment.viewKanban pipeline (grouped by status)
GET/api/job-applicants/{id}hr.recruitment.viewOne applicant
POST/api/job-applicantshr.recruitment.manageCreate (JSON — see deep-dive)
PUT/api/job-applicants/{id}hr.recruitment.manageUpdate
PATCH/api/job-applicants/{id}/statushr.recruitment.manageMove through the funnel
DELETE/api/job-applicants/{id}hr.recruitment.manageDelete
POST/api/job-applicants/{id}/resumehr.recruitment.manageUpload resume (multipart)
GET/api/job-applicants/{id}/resumehr.recruitment.viewDownload resume
DELETE/api/job-applicants/{id}/resumehr.recruitment.manageRemove resume
GET/api/job-applicants/{id}/match-scorehr.recruitment.viewATS match score vs. the opening

Deep-dive: POST /api/job-applicants

Creating an applicant is plain JSON — the resume is not part of this request. Upload it afterwards to POST /api/job-applicants/{id}/resume as multipart/form-data (the parsed resume also feeds the match-score endpoint).

Request (JobApplicantCreateRequestopeningId, firstName, lastName required, the rest optional):

{
"openingId": 12,
"firstName": "Maria",
"lastName": "Santos",
"email": "[email protected]",
"phone": "0917-555-9876",
"birthday": "1998-06-02",
"address": "Cebu City",
"skills": "Java, Spring Boot, SQL",
"expectedSalary": 45000,
"referredById": 10007,
"notes": "Referred by engineering"
}

Responses: 200 with the created JobApplicantDto; 400 on validation failure; 404 if openingId doesn't exist.

Job offers — /api/job-offers

JobOfferController.java

MethodPathPermissionPurpose
GET/api/job-offers · /{id}hr.recruitment.viewPaged list / one offer
POST/api/job-offershr.recruitment.offer.manageCreate an offer for an applicant
PATCH/api/job-offers/{id}/hiring-detailshr.recruitment.offer.manageFill compensation + government IDs pre-hire
PATCH/api/job-offers/{id}/statushr.recruitment.offer.manageDecide the offer (see deep-dive)
DELETE/api/job-offers/{id}hr.recruitment.offer.manageDelete

Deep-dive: PATCH /api/job-offers/{id}/status (offer decision)

{ "status": "Accepted" }

For any status other than Accepted this is a plain status write. For Accepted, the backend runs the hire automation — verified in JobOfferServiceImpl.updateStatus — in one transaction:

  1. Validates the offer + applicant carry everything an employee record requires (compensation figures, government IDs from hiring-details, birthday/address from the applicant) — missing data fails the whole call with 400 and nothing is written.
  2. Creates the employee from the offer + applicant data.
  3. Creates a login user for the new employee with a generated temporary password.
  4. Pushes an ONBOARDING notification to the new employee.
  5. Flips the applicant's status to Hired.

The response JobOfferDto then carries three extra fields the UI shows once: hiredEmployeeNumber, hiredUsername, and hiredTemporaryPassword (the temporary password is not retrievable again — hand it to the hire out-of-band).

Interviews — /api/interview-schedules, /api/interview-feedback

InterviewScheduleController.java, InterviewFeedbackController.java

MethodPathPermissionPurpose
GET/api/interview-schedules · /{id}hr.recruitment.viewPaged list / one schedule
POST · PUT /{id} · PATCH /{id}/status · DELETE /{id}/api/interview-schedules...hr.recruitment.interview.manageSchedule / edit / progress / remove
GET/api/interview-schedules/{id}/feedbackhr.recruitment.viewFeedback for an interview
POST/api/interview-schedules/{id}/feedbackhr.recruitment.interview.manageAdd feedback
PUT · DELETE/api/interview-feedback/{id}hr.recruitment.interview.manage (class-level)Edit / delete a feedback entry

Staffing plans — /api/staffing-plans

StaffingPlanController.java — class-level hr.recruitment.staffing.manage on everything: GET / (paged), GET /{id}, POST, PUT /{id}, DELETE /{id}, plus line items via POST /{id}/items and DELETE /{id}/items/{itemId}.

Public careers — /api/public (no auth)

PublicJobController.java — powers the marketing site's careers pages; no token required.

MethodPathPurpose
GET/api/public/job-openings?page=&size=Open postings only, newest first — returns a plain JSON array (no page envelope)
GET/api/public/job-openings/{id}One posting; 404 for any opening that isn't Open (drafts/closed are invisible)
POST/api/public/job-applicationsSubmit an application: {openingId, firstName, lastName, email, phone, coverLetter} — creates an applicant (coverLetter lands in the applicant's notes)

The public application intake takes no resume; recruiters attach one later via the authenticated resume-upload endpoint.