Last reviewed: 2026-05-19.
ScoutLane has two callable surfaces:
- REST endpoints under
src/app/api/*. - Server Actions under
src/server/services/*.
Most admin mutations are Server Actions. REST routes are used for auth, health, public JSON application submit, admin reads, exports, integration testing/retry, parse retry, applicant rescoring, and job alerts.
For the take-home assessment's "agentic future" requirement, the current app is callable but not fully REST-complete. Treat Server Actions as the current internal command surface and the API parity checklist below as the work required before external agents can safely operate every workflow.
Public endpoints bypass auth only when explicitly listed in src/middleware.ts. Admin routes require an authenticated workspace role and many handlers also scope data by session.user.organizationId.
Workspace roles:
ADMINRECRUITERHIRING_MANAGER
Organization/team settings mutations require ADMIN.
The API is not fully normalized.
- Server Actions and public routes commonly return
{ success: boolean, error?: string }. - Admin REST routes often return
{ error: string }on failure.
Client code should check response.ok first.
Target normalized shape for future endpoints:
{
"success": true,
"data": {},
"error": {
"code": "VALIDATION_ERROR",
"message": "Human-safe error message"
}
}Only one of data or error should be present. Error messages returned to candidates should stay generic and must not expose stack traces, SQL errors, provider responses, internal paths, tokens, or secrets.
Open health check.
Response:
{ "status": "ok", "timestamp": "2026-05-19T00:00:00.000Z" }Auth.js handler. Providers are configured in src/lib/auth/auth.config.ts and src/lib/auth/auth.ts.
Returns custom application fields for a job.
Response:
{ "customFields": [] }Saves the job's application form fields. Authenticated + org-scoped. Body is
validated with the shared customFieldsSchema (src/schemas/template.ts).
Accepts either a bare array or { "customFields": [...] }.
Body:
{ "customFields": [{ "id": "city", "label": "City", "type": "text", "required": false }] }Responses: 200 { success: true }, 400 invalid fields, 401/403 auth,
404 job not found / not in org. Delegates to saveCustomFieldsImpl.
REST parity for the Kanban move. Authenticated + org-scoped. Delegates to
moveApplicantImpl, which updates stage/status, creates a StageTransition,
and dispatches active webhooks + the per-stage integration.
Body:
{ "targetStageId": "stage_id" }Responses: 200 { success: true }, 400 invalid body / invalid stage,
401/403 auth, 404 applicant not found / not in org.
Returns ordered pipeline stages with applicants grouped into each stage.
Applicants without pipelineStageId are placed in the first stage.
Creates a per-stage outbound integration.
Body:
{
"stageId": "stage_id",
"endpointUrl": "https://example.com/scoutlane",
"apiKey": "optional-token",
"includeQuestions": false
}Sends a synthetic stage_transition payload to the integration URL and records an IntegrationLog.
Retries the most recent failed or recent transition payload for the integration.
Deletes a job integration after verifying ownership.
Re-enqueues resume parsing for an applicant. Sets parsing status to PENDING before enqueue. Returns FAILED if enqueue itself fails.
Re-runs applicant match scoring for an applicant using the current parsed data and job context.
Downloads applicants for a job as CSV.
Columns:
id,name,email,phone,pipelineStage,status,appliedAt,resumeUrl,parsingStatus
Public JSON-only application submit. This endpoint does not upload files and is not the primary production apply flow. The real public form uses the submitJobApplication Server Action with multipart form data.
Rejects resumeUrl in the body.
Public status lookup. Returns data: null instead of 404 when the id is not found or does not match the job.
Subscribes an email address to public job alerts. Backed by JobAlert.
Files: src/server/services/jobs/*
createJob(formData)updateJob(id, data)deleteJob(id)getJob(id)saveCustomFields(jobId, customFields)
createJob can snapshot template data into the new job, including stages, questions, custom fields, and description fields.
Files: src/server/services/pipeline/*
getPipelineData(jobId)createStage(jobId, name, color?)updateStage(stageId, data)deleteStage(stageId, reassignToStageId?)reorderStages(stages)moveApplicant(applicantId, newStageId)
moveApplicant is the central pipeline operation. It updates applicant stage/status, creates StageTransition, dispatches active webhooks, and dispatches configured per-stage integrations.
Files: src/server/services/applicants/*
getApplicants(filters)getApplicantDetail(applicantId)updateApplicantStatus(applicantId, status)updateApplicantNotes(applicantId, notes)saveApplicantResumeDataJson(applicantId, jsonText)updateInterviewDate(applicantId, interviewDate | null)createApplicantNote(applicantId, body)updateApplicantNote(noteId, body)deleteApplicantNote(noteId)
File: src/server/services/templates.ts
createTemplate()updateTemplate(id, formData)duplicateTemplate(id)deleteTemplate(id)
Templates are copied into jobs at creation time. Existing jobs are not linked live to template changes.
File: src/server/services/settings.ts
updateMyProfile(formData)updateOrganizationSettings(formData)updateTeamMemberRole(formData)
Files:
src/server/services/submit-job-application.tssrc/server/services/submit-job-application-impl.ts
Flow:
- Validate
FormDatawith Zod. - Upload resume.
- Create applicant.
- Enqueue
resume.parse. - Send confirmation email.
| Capability | Current Surface | REST Parity Needed | Priority |
|---|---|---|---|
| Create/update/delete jobs | Server Actions | Yes: authenticated job command endpoints | P1 |
| Publish/close/archive jobs | Server Actions/service logic | Yes: explicit status endpoints or job update command | P1 |
| Custom job form fields | REST read + write (POST .../form) + Server Action |
Done: authenticated write endpoint with customFieldsSchema validation |
Done |
| Pipeline stage CRUD/reorder | Server Actions | Yes: stage create/update/delete/reorder endpoints | P1 |
| Move applicant | REST (POST .../move) + Server Action |
Done: audited move endpoint creates StageTransition + dispatches webhooks/integration |
Done |
| Applicant notes/interview date | Server Actions | Yes: note and applicant update endpoints | P1 |
| Parsed resume correction | Server Action | Yes: JSON/schema-safe correction endpoint | P1 |
| Parse retry | REST | Already REST exposed | Done |
| Applicant rescore | REST | Already REST exposed | Done |
| Applicant CSV export | REST | Already REST exposed | Done |
| Templates | Server Actions | Yes: CRUD/duplicate/preview endpoints | P1 |
| Integrations test/retry/delete | REST | Create/test/retry/delete mostly REST exposed | Partial |
| Organization/team settings | Server Actions | Yes: admin-only settings endpoints | P2 |
- Every admin endpoint must call an auth/session helper and verify organization or job ownership.
- Middleware is a routing convenience, not the only security boundary.
- Every request body should be validated with Zod at the route/action boundary.
- Public endpoints need rate limits before production use.
- Webhook and integration endpoints intentionally send candidate data to configured external systems; logs and UI should mask bearer tokens and avoid raw secret exposure.
Organization webhook event:
{
"event": "applicant.status_changed",
"timestamp": "2026-05-19T00:00:00.000Z",
"data": {
"applicantId": "app_id",
"name": "Alex Doe",
"email": "alex@example.com",
"stageId": "stage_id",
"stageName": "Interview",
"status": "INTERVIEW",
"jobTitle": "Senior Engineer"
}
}Per-stage integration event:
{
"event": "stage_transition",
"timestamp": "2026-05-19T00:00:00.000Z",
"candidate": {
"id": "app_id",
"name": "Alex Doe",
"email": "alex@example.com",
"phone": "+1 555 0100",
"resumeUrl": "https://..."
},
"assessment": {
"title": "Senior Engineer",
"description": "Please answer each question concisely.",
"questions": []
}
}