REST API for the InterimPlaza recruitment platform (product label under Mahmoud Consultancy B.V.; built by GloryLabs — see root CLAUDE.md → Corporate Structure).
This reference is generated from the Spring Boot controllers in mahmoud-consultancy/backend. It is the canonical workspace API doc; for system design see architecture.md and for local setup see setup.md.
| Property | Value |
|---|---|
| Base URL (local) | http://localhost:8090 |
| Base URL (prod) | https://app.interimplaza.nl (backend behind shared ingress) |
| Context path | none — endpoints are served at the application root |
| Content type | application/json unless noted (file upload/download differ) |
| Auth scheme | JWT Bearer — Authorization: Bearer <accessToken> |
| Session policy | Stateless (SessionCreationPolicy.STATELESS) |
| Pagination | Spring Data Pageable — page, size, sort query params; responses are Page<T> envelopes (content, totalElements, totalPages, number, size) |
⚠️ Path inconsistency (known). The codebase mixes two path styles: some controllers are mounted at the root (
/jobs,/applications,/articles,/job-alerts,/crawler) while newer admin/CV controllers use/api/...and/api/v1/...prefixes. Paths below are documented exactly as the controllers declare them. A future cleanup should normalise everything under/api/v1.
POST /api/auth/register or POST /api/auth/login → returns AuthResponse with accessToken + refreshToken.Authorization: Bearer <accessToken> on protected requests.POST /api/auth/refresh with the refresh token.POST /api/auth/mfa/verify-login to obtain tokens.| Tier | Rule | Routes |
|---|---|---|
| Public | permitAll() | /api/auth/**, /auth/**, /jobs/**, /api/jobs/**, /api/public/**, /crawler/**, swagger/actuator-health |
| Authenticated | valid JWT | /applications/**, /articles/**, /job-alerts/**, /api/auth/me, MFA management, per-user bookmarks |
| Recruiter | ROLE_ADMIN or ROLE_RECRUITER | /api/v1/admin/applications/**, /api/v1/admin/cv-profiles/**, /api/v1/admin/statistics, /admin/articles/** |
| Admin | ROLE_ADMIN | /api/v1/admin/jobs/**, /api/v1/admin/users/**, /api/v1/admin/sources/**, article delete |
Ownership checks (a user may only read/modify their own applications, alerts, CV profiles) are enforced in the service layer and surface as 404 Not Found on unauthorized access rather than 403.
| Status | Meaning |
|---|---|
| 400 | Validation failure (Bean Validation) / malformed body |
| 401 | Missing or invalid JWT |
| 403 | Authenticated but lacks the required role |
| 404 | Not found, or owned-by-another-user (ownership-guarded resources) |
| 409 | Conflict (e.g. duplicate registration) |
| 413 | Payload too large (CV upload > 5 MB) |
AuthControllerBase path /api/auth · public (permitAll)
| Method | Path | Auth | Request | Response |
|---|---|---|---|---|
| POST | /api/auth/register | public | RegisterRequest | AuthResponse (tokens) |
| POST | /api/auth/login | public | LoginRequest | AuthResponse (tokens, or MFA challenge) |
| POST | /api/auth/refresh | public | RefreshRequest | AuthResponse |
| POST | /api/auth/logout | bearer | — (uses Authorization header) | 200 |
| GET | /api/auth/verify-email | public | query token | String |
| POST | /api/auth/forgot-password | public | ForgotPasswordRequest | String |
| POST | /api/auth/reset-password | public | ResetPasswordRequest | String |
| GET | /api/auth/me | bearer | — | UserDto |
POST /api/auth/login — example
POST /api/auth/login
Content-Type: application/json
{ "email": "user@example.com", "password": "••••••••" }
{
"accessToken": "eyJhbGciOi…",
"refreshToken": "eyJhbGciOi…",
"tokenType": "Bearer",
"user": { "id": 1, "email": "user@example.com", "role": "CANDIDATE" }
}
MfaControllerBase path /api/auth/mfa · authenticated (except verify-login)
| Method | Path | Auth | Request | Response |
|---|---|---|---|---|
| POST | /api/auth/mfa/setup | bearer | — | TotpSetupResponse (secret + QR) |
| POST | /api/auth/mfa/enable | bearer | TotpVerifyRequest (code) | 200 |
| POST | /api/auth/mfa/disable | bearer | TotpDisableRequest (password) | 200 |
| GET | /api/auth/mfa/status | bearer | — | TotpStatusResponse |
| POST | /api/auth/mfa/verify-login | public | MfaLoginRequest | AuthResponse (tokens) |
| POST | /api/auth/mfa/backup-codes/regenerate | bearer | TotpVerifyRequest | List<String> (backup codes) |
JobControllerBase path /jobs · public browse (permitAll); per-user actions authenticated
| Method | Path | Auth | Request | Response |
|---|---|---|---|---|
| GET | /jobs | public | query page,size,sortBy,sortDirection | Page<JobDto> |
| GET | /jobs/search | public | query query,page,size | Page<JobDto> |
| POST | /jobs/filter | public | JobFilterDto (+ query page,size) | Page<JobDto> |
| GET | /jobs/{id} | public | — | JobDto |
| POST | /jobs | bearer (recruiter, service-enforced) | JobDto | JobDto (201) |
| PUT | /jobs/{id} | bearer (recruiter) | JobDto | JobDto |
| DELETE | /jobs/{id} | bearer (recruiter) | — | 204 |
| GET | /jobs/filters | public | — | Map<String,Object> (available filter values) |
| GET | /jobs/statistics | public | — | Map<String,Object> |
| GET | /jobs/{id}/sources | public | — | List<JobSourceLinkDto> |
| GET | /jobs/{id}/similar | public | query limit (5) | List<JobDto> |
| POST | /jobs/{id}/bookmark | bearer | — | 200 |
| POST | /jobs/{id}/unbookmark | bearer | — | 200 |
| GET | /jobs/{id}/bookmark | bearer | — | Boolean |
| GET | /jobs/bookmarks | bearer | Pageable | Page<JobDto> |
ApplicationControllerBase path /applications · authenticated
| Method | Path | Auth | Request | Response |
|---|---|---|---|---|
| POST | /applications | bearer | ApplicationDto | ApplicationDto (201) |
| GET | /applications/my-applications | bearer | — | List<ApplicationDto> |
| GET | /applications/check/{jobId} | bearer | — | ApplicationCheckDto |
| GET | /applications/job/{jobId} | bearer (recruiter) | — | List<ApplicationDto> |
| PUT | /applications/{id}/status | bearer | query status | ApplicationDto |
| GET | /applications/{id}/history | bearer | — | List<ApplicationStatusHistoryDto> |
| DELETE | /applications/{id} | bearer (candidate) | — | ApplicationDto (withdrawn) |
| GET | /applications/statistics | bearer (recruiter) | — | Map<String,Object> |
| GET | /applications | bearer | query status | List<ApplicationDto> |
CvUploadControllerBase path /api/applications · authenticated
| Method | Path | Auth | Request | Response |
|---|---|---|---|---|
| POST | /api/applications/upload-cv | bearer | multipart/form-data field file (PDF, ≤ 5 MB) | Map {fileName, filePath} |
ArticleControllerBase path /articles · authenticated
| Method | Path | Request | Response |
|---|---|---|---|
| GET | /articles | Pageable (sort publishedAt) | Page<ArticleDto> |
| GET | /articles/{slug} | — (increments view count) | ArticleDto |
| GET | /articles/category/{category} | Pageable | Page<ArticleDto> |
| GET | /articles/search | query q | Page<ArticleDto> |
| GET | /articles/{id}/related | query limit (5) | List<ArticleDto> |
| GET | /articles/popular | query limit (10) | List<ArticleDto> |
| GET | /articles/recent | query limit (10) | List<ArticleDto> |
| POST | /articles/{id}/rate | ArticleRatingDto | ArticleRatingDto |
| GET | /articles/{id}/ratings | Pageable | Page<ArticleRatingDto> |
| POST | /articles/{id}/bookmark | — | 200 |
| POST | /articles/{id}/unbookmark | — | 200 |
| GET | /articles/bookmarks | Pageable | Page<ArticleDto> |
JobAlertControllerBase path /job-alerts · authenticated
| Method | Path | Request | Response |
|---|---|---|---|
| POST | /job-alerts | JobAlertDto | JobAlertDto (201) |
| GET | /job-alerts | — | List<JobAlertDto> |
| GET | /job-alerts/{id} | — | JobAlertDto |
| PUT | /job-alerts/{id} | JobAlertDto | JobAlertDto |
| DELETE | /job-alerts/{id} | — | 204 |
| GET | /job-alerts/{id}/matches | query page (0), size (20) | Page<JobDto> |
CVProfileController (cv-service)Base path /api/v1/cv-profiles · authenticated (JWT bearer)
| Method | Path | Request | Response |
|---|---|---|---|
| GET | /api/v1/cv-profiles | — | List<CVProfileDTO> |
| GET | /api/v1/cv-profiles/{profileId} | — | CVProfileDTO |
| GET | /api/v1/cv-profiles/{profileId}/full | — | CVProfileDTO (with relations) |
| GET | /api/v1/cv-profiles/default | — | CVProfileDTO |
| POST | /api/v1/cv-profiles | CVProfileDTO | CVProfileDTO (201) |
| PUT | /api/v1/cv-profiles/{profileId} | CVProfileDTO | CVProfileDTO |
| DELETE | /api/v1/cv-profiles/{profileId} | — | 204 |
| POST | /api/v1/cv-profiles/{profileId}/duplicate | query newName | CVProfileDTO (201) |
| PUT | /api/v1/cv-profiles/{profileId}/set-default | — | Map {message} |
| GET | /api/v1/cv-profiles/{profileId}/generate-pdf | query download (true) | byte[] (application/pdf) |
CrawlerControllerBase path /crawler · public (permitAll) — used by the scraping subsystem
| Method | Path | Request | Response |
|---|---|---|---|
| POST | /crawler/crawl | CrawlRequest {url, crawlType?, maxPages?, initiatedBy} | Map {crawlJobId, status, message} |
| GET | /crawler/status/{id} | — | CrawlJob |
| GET | /crawler/history | query page,size | Page<CrawlJob> |
| POST | /crawler/trigger-scraping | — | Map {success, message} |
| POST | /crawler/trigger-flextender | — | Map {success, message} |
| GET | /crawler/scraping-status | — | ScrapingStatus |
| POST | /crawler/scrape | ScrapeRequest {url, createJob, company?, location?} | Map {success, title, content, url} |
The
/crawler/**routes arepermitAllat the application layer. In production they should be network-restricted (internal/scheduler use only); do not expose them publicly without an upstream guard.
All routes below require a JWT with the indicated role.
AdminControllerBase path /api/v1/admin · ROLE_ADMIN or ROLE_RECRUITER
| Method | Path | Request | Response |
|---|---|---|---|
| GET | /api/v1/admin/statistics | — | Map (totalUsers, newUsersThisMonth, totalJobs, activeJobs, totalApplications, pendingApplications, totalCVProfiles) |
| GET | /api/v1/admin/statistics/applications-by-status | — | Map<String,Long> |
| GET | /api/v1/admin/statistics/monthly-applications | query months (6) | List<Map> |
| GET | /api/v1/admin/statistics/top-jobs | query limit (5) | List<Map> |
| GET | /api/v1/admin/system/health | — | Map (status, timestamp, database, emailService) |
AdminApplicationControllerBase path /api/v1/admin/applications · ROLE_ADMIN or ROLE_RECRUITER
| Method | Path | Request | Response |
|---|---|---|---|
| GET | /api/v1/admin/applications | Pageable | Page<ApplicationDto> |
| GET | /api/v1/admin/applications/by-status/{status} | Pageable | Page<ApplicationDto> |
| PUT | /api/v1/admin/applications/{id}/status | Map {status, message?, notes?} | ApplicationDto (sends email) |
| PUT | /api/v1/admin/applications/{id}/notes | Map {notes} | ApplicationDto |
| GET | /api/v1/admin/applications/by-job/{jobId} | Pageable | Page<ApplicationDto> |
| GET | /api/v1/admin/applications/{id}/cv | — | Resource (inline PDF) |
| GET | /api/v1/admin/applications/by-user/{userId} | Pageable | Page<ApplicationDto> |
| DELETE | /api/v1/admin/applications/{id} | — | 204 |
| PUT | /api/v1/admin/applications/bulk-update | Map {ids[], status, message?} | Map<String,Object> |
AdminJobControllerBase path /api/v1/admin/jobs · ROLE_ADMIN
| Method | Path | Request | Response |
|---|---|---|---|
| GET | /api/v1/admin/jobs | query page,size,search?,active? | Page<JobDto> |
| POST | /api/v1/admin/jobs | JobDto | JobDto (201) |
| PUT | /api/v1/admin/jobs/{id} | JobDto | JobDto |
| PUT | /api/v1/admin/jobs/{id}/toggle | — | JobDto (flips active) |
| DELETE | /api/v1/admin/jobs/{id} | — | 204 |
AdminArticleControllerBase path /admin/articles · ROLE_ADMIN or ROLE_RECRUITER (delete: ROLE_ADMIN)
| Method | Path | Request | Response |
|---|---|---|---|
| GET | /admin/articles | Pageable | Page<ArticleDto> (incl. unpublished) |
| GET | /admin/articles/{id} | — | ArticleDto |
| POST | /admin/articles | CreateArticleRequest | ArticleDto (201) |
| PUT | /admin/articles/{id} | UpdateArticleRequest | ArticleDto |
| DELETE | /admin/articles/{id} | — (admin only) | 204 |
| POST | /admin/articles/{id}/publish | — | ArticleDto |
| POST | /admin/articles/{id}/unpublish | — | ArticleDto |
AdminJobSourceControllerBase path /api/v1/admin/sources · ROLE_ADMIN
| Method | Path | Request | Response |
|---|---|---|---|
| GET | /api/v1/admin/sources | Pageable | Page<JobSourceDto> |
| GET | /api/v1/admin/sources/type/{sourceType} | Pageable | Page<JobSourceDto> |
| GET | /api/v1/admin/sources/search | query query | Page<JobSourceDto> |
| GET | /api/v1/admin/sources/{id} | — | JobSourceDto |
| POST | /api/v1/admin/sources | JobSourceDto | JobSourceDto (201) |
| PUT | /api/v1/admin/sources/{id} | JobSourceDto | JobSourceDto |
| DELETE | /api/v1/admin/sources/{id} | — | 204 |
| PUT | /api/v1/admin/sources/{id}/toggle | — | JobSourceDto |
| POST | /api/v1/admin/sources/{id}/scrape | — | ScrapeResult |
| POST | /api/v1/admin/sources/scrape-all | — | String |
AdminCVProfileControllerBase path /api/v1/admin/cv-profiles · ROLE_ADMIN or ROLE_RECRUITER
| Method | Path | Request | Response |
|---|---|---|---|
| GET | /api/v1/admin/cv-profiles | Pageable | Page<CVProfileDTO> |
| GET | /api/v1/admin/cv-profiles/by-user/{userId} | — | List<CVProfileDTO> |
| GET | /api/v1/admin/cv-profiles/{id} | — | CVProfileDTO |
| GET | /api/v1/admin/cv-profiles/{id}/full | — | CVProfile (entity, nested) |
| GET | /api/v1/admin/cv-profiles/{id}/download-pdf | — | byte[] (PDF attachment) |
| DELETE | /api/v1/admin/cv-profiles/{id} | — | 204 |
| GET | /api/v1/admin/cv-profiles/recent | query days (7) | Page<CVProfileDTO> |
| GET | /api/v1/admin/cv-profiles/search | query query | Page<CVProfileDTO> |
AdminUserControllerBase path /api/v1/admin/users · ROLE_ADMIN
| Method | Path | Request | Response |
|---|---|---|---|
| GET | /api/v1/admin/users | Pageable | Page<UserDto> |
| GET | /api/v1/admin/users/search | query query | Page<UserDto> |
| GET | /api/v1/admin/users/{id} | — | UserDto |
| PUT | /api/v1/admin/users/{id}/role | query role | UserDto |
| PUT | /api/v1/admin/users/{id}/status | query enabled (boolean) | UserDto |
| DELETE | /api/v1/admin/users/{id} | — | 204 (soft-delete) |
| GET | /api/v1/admin/users/recent | query days (7) | List<UserDto> |
mahmoud-consultancy/backend/src/main/java/nl/glorylabs/controller/ and the
cv-service module. When endpoints change, regenerate this table or, better,
rely on the live OpenAPI spec./v3/api-docs and Swagger UI at
/swagger-ui/** (both public). Use those for the always-current contract,
including full DTO field shapes./api/v1
prefix to remove the root-vs-prefixed path inconsistency noted above.
Reacties