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
| Method | Path | Permission | Purpose |
|---|---|---|---|
| GET | /api/job-requisitions | hr.recruitment.view | Paged/filtered list |
| GET | /api/job-requisitions/{id} | hr.recruitment.view | One requisition |
| POST | /api/job-requisitions | hr.recruitment.manage | Create |
| PUT | /api/job-requisitions/{id} | hr.recruitment.manage | Update |
| PATCH | /api/job-requisitions/{id}/status | hr.recruitment.manage | Move through the approval workflow |
| PATCH | /api/job-requisitions/{id}/archive · /restore | hr.recruitment.manage | Soft-delete / restore |
| DELETE | /api/job-requisitions/{id} | hr.recruitment.manage | Delete |
Job openings — /api/job-openings
| Method | Path | Permission | Purpose |
|---|---|---|---|
| GET | /api/job-openings · /{id} | hr.recruitment.view | Paged list / one opening |
POST · PUT /{id} | /api/job-openings... | hr.recruitment.manage | Create / update |
| PATCH | /api/job-openings/{id}/status | hr.recruitment.manage | Open / close the posting |
| DELETE | /api/job-openings/{id} | hr.recruitment.manage | Delete |
Openings with status Open are what the public careers endpoints expose.
Job applicants — /api/job-applicants
| Method | Path | Permission | Purpose |
|---|---|---|---|
| GET | /api/job-applicants | hr.recruitment.view | Paged/filtered list |
| GET | /api/job-applicants/pipeline | hr.recruitment.view | Kanban pipeline (grouped by status) |
| GET | /api/job-applicants/{id} | hr.recruitment.view | One applicant |
| POST | /api/job-applicants | hr.recruitment.manage | Create (JSON — see deep-dive) |
| PUT | /api/job-applicants/{id} | hr.recruitment.manage | Update |
| PATCH | /api/job-applicants/{id}/status | hr.recruitment.manage | Move through the funnel |
| DELETE | /api/job-applicants/{id} | hr.recruitment.manage | Delete |
| POST | /api/job-applicants/{id}/resume | hr.recruitment.manage | Upload resume (multipart) |
| GET | /api/job-applicants/{id}/resume | hr.recruitment.view | Download resume |
| DELETE | /api/job-applicants/{id}/resume | hr.recruitment.manage | Remove resume |
| GET | /api/job-applicants/{id}/match-score | hr.recruitment.view | ATS 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 (JobApplicantCreateRequest — openingId, firstName, lastName
required, the rest optional):
{
"openingId": 12,
"firstName": "Maria",
"lastName": "Santos",
"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
| Method | Path | Permission | Purpose |
|---|---|---|---|
| GET | /api/job-offers · /{id} | hr.recruitment.view | Paged list / one offer |
| POST | /api/job-offers | hr.recruitment.offer.manage | Create an offer for an applicant |
| PATCH | /api/job-offers/{id}/hiring-details | hr.recruitment.offer.manage | Fill compensation + government IDs pre-hire |
| PATCH | /api/job-offers/{id}/status | hr.recruitment.offer.manage | Decide the offer (see deep-dive) |
| DELETE | /api/job-offers/{id} | hr.recruitment.offer.manage | Delete |
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:
- 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. - Creates the employee from the offer + applicant data.
- Creates a login user for the new employee with a generated temporary password.
- Pushes an
ONBOARDINGnotification to the new employee. - 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
| Method | Path | Permission | Purpose |
|---|---|---|---|
| GET | /api/interview-schedules · /{id} | hr.recruitment.view | Paged list / one schedule |
POST · PUT /{id} · PATCH /{id}/status · DELETE /{id} | /api/interview-schedules... | hr.recruitment.interview.manage | Schedule / edit / progress / remove |
| GET | /api/interview-schedules/{id}/feedback | hr.recruitment.view | Feedback for an interview |
| POST | /api/interview-schedules/{id}/feedback | hr.recruitment.interview.manage | Add 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.
| Method | Path | Purpose |
|---|---|---|
| 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-applications | Submit 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.