Skip to content

Recruitment API

All authenticated endpoints require a valid Cloudflare Access JWT and appropriate permissions.

Base paths:

  • Authenticated: /api/recruitment
  • Public (no auth): /api/careers

Returns all published jobs for the public careers page.

Response

{
"jobs": [
{
"id": "string",
"title": "string",
"department": "string | null",
"location": "string | null",
"workplace_type": "hybrid",
"employment_type": "full_time",
"experience_level": "string | null",
"published_at": "string | null"
}
]
}

Returns a single published job with full details (description, requirements, benefits).

Submit a public application. Upserts the candidate (by email) and creates an application.

Request

{
"job_id": "string (required)",
"first_name": "string (required)",
"last_name": "string (required)",
"email": "string (required)",
"phone": "string",
"location": "string",
"linkedin_url": "string",
"cover_letter": "string",
"source_channel": "string"
}

Response 200

{ "success": true, "application_id": "string" }

Error 409 — duplicate application for same job+candidate.


List jobs. Filterable by status.

Query params: status (draft, published, paused, closed, filled, or omit for all)

Permission: recruitment:view

Response includes stage_counts array with candidate counts per pipeline stage.

Get job detail with pipeline stages (including candidate counts) and hiring team.

Response

{
"job": { "..." },
"stages": [
{ "id": "string", "name": "string", "position": 0, "candidate_count": 5 }
],
"team": [
{ "user_id": "string", "role": "recruiter", "name": "string", "email": "string" }
]
}

Create a new job. Defaults to draft status with the default pipeline template.

Permission: recruitment:manage

Update job fields (title, description, location, etc.).

Permission: recruitment:manage

Transition job status. Valid transitions:

  • draftpublished
  • publishedpaused, closed
  • pausedpublished, closed
  • closeddraft

Request: { "status": "published" }


List applications for a job with joined candidate info and stage details.

Query params: stage_id, status (default: active)

Aggregate stats: total, active, rejected, hired, new_this_week.


List hiring team members for a job.

Add a team member. Request: { "user_id": "string", "role": "recruiter" }

Roles: recruiter, hiring_manager, interviewer, coordinator

DELETE /api/recruitment/jobs/:id/team/:userId

Section titled “DELETE /api/recruitment/jobs/:id/team/:userId”

Remove a team member.


GET /api/recruitment/jobs/:id/requirements

Section titled “GET /api/recruitment/jobs/:id/requirements”

List requirements for a job (used in AI scoring).

PUT /api/recruitment/jobs/:id/requirements

Section titled “PUT /api/recruitment/jobs/:id/requirements”

Bulk replace requirements. Request: { "requirements": [...] }


Search candidates with pagination.

Query params: search, source, job_id, stage_id, page

Get candidate with their applications and activity history.

Create a candidate. Returns { candidate, existing: boolean } — if the email exists, returns the existing record.

Update candidate fields.


Create an application (candidate × job). Assigns to the entry stage of the job’s pipeline.

Request: { "job_id": "string", "candidate_id": "string", "source_channel": "string" }

PUT /api/recruitment/applications/:id/stage

Section titled “PUT /api/recruitment/applications/:id/stage”

Move an application to a new pipeline stage. Creates an audit trail entry.

Request: { "stage_id": "string", "note": "string" }

PUT /api/recruitment/applications/:id/reject

Section titled “PUT /api/recruitment/applications/:id/reject”

Reject an application. Records the rejection stage and reason.

Request: { "rejection_reason": "string" }

PUT /api/recruitment/applications/:id/withdraw

Section titled “PUT /api/recruitment/applications/:id/withdraw”

Withdraw an application (candidate-initiated).

PUT /api/recruitment/applications/:id/hire

Section titled “PUT /api/recruitment/applications/:id/hire”

Hire the candidate. Creates a user account, links candidate record, assigns onboarding template, and logs the hire activity.

Request:

{
"start_date": "2025-04-01",
"job_title": "Senior Engineer",
"access_level": "employee",
"onboarding_template_id": "tpl-onb-general"
}

Response:

{
"success": true,
"user_id": "string",
"existing_user": false
}

GET /api/recruitment/applications/:id/timeline

Section titled “GET /api/recruitment/applications/:id/timeline”

Get activity timeline for an application.

POST /api/recruitment/applications/:id/notes

Section titled “POST /api/recruitment/applications/:id/notes”

Add a note to an application. Request: { "note": "string" }


Global activity stream. Query params: job_id, action, page


List all pipeline templates with their stages.

GET /api/recruitment/pipeline-templates/:id

Section titled “GET /api/recruitment/pipeline-templates/:id”

Get a single template with stages.


List all email templates.

Permission: recruitment:view

Response

{
"templates": [
{
"id": "string",
"name": "string",
"subject": "string",
"body": "string (HTML)",
"category": "general | thank_you | interview | rejection | offer",
"is_active": 1,
"created_by": "string",
"created_at": "string",
"updated_at": "string"
}
]
}

Get a single email template.

Create an email template.

Permission: recruitment:manage

Request

{
"name": "string (required)",
"subject": "string (required)",
"body": "string (required, HTML with {{merge.fields}})",
"category": "general (default)"
}

Update an email template.

Permission: recruitment:manage

DELETE /api/recruitment/email-templates/:id

Section titled “DELETE /api/recruitment/email-templates/:id”

Delete an email template.

Permission: recruitment:manage

POST /api/recruitment/email-templates/:id/preview

Section titled “POST /api/recruitment/email-templates/:id/preview”

Preview a template with merge fields resolved using a sample candidate.

Permission: recruitment:view

Request

{
"candidate_id": "string (required)",
"application_id": "string (optional)"
}

Response

{
"subject": "string (resolved)",
"body": "string (HTML, resolved)"
}

POST /api/recruitment/candidates/:id/send-email

Section titled “POST /api/recruitment/candidates/:id/send-email”

Send an email to a candidate via Gmail API. Records in communication log and activity feed.

Permission: recruitment:update

Request

{
"subject": "string (required)",
"body": "string (required, HTML)",
"application_id": "string (optional)",
"template_id": "string (optional)"
}

Response

{
"communication_id": "string",
"gmail_message_id": "string"
}

GET /api/recruitment/candidates/:id/communications

Section titled “GET /api/recruitment/candidates/:id/communications”

Get communication log for a candidate.

Permission: recruitment:view

Response

{
"communications": [
{
"id": "string",
"subject": "string",
"body": "string",
"to_email": "string",
"from_email": "string",
"status": "sent | failed",
"sent_at": "string",
"sent_by_name": "string | null",
"template_name": "string | null"
}
]
}

Send personalised emails to multiple candidates. Merge fields are resolved per candidate.

Permission: recruitment:manage

Request

{
"candidate_ids": ["string"],
"subject": "string (required)",
"body": "string (required, HTML with {{merge.fields}})",
"template_id": "string (optional)"
}

GET /api/recruitment/pipeline-templates/:id/email-rules

Section titled “GET /api/recruitment/pipeline-templates/:id/email-rules”

Get auto-send email rules for a pipeline template.

Permission: recruitment:view

Response

{
"rules": [
{
"id": "string",
"stage_id": "string",
"template_id": "string",
"trigger_on": "enter",
"is_active": 1
}
]
}

PUT /api/recruitment/pipeline-templates/:id/email-rules

Section titled “PUT /api/recruitment/pipeline-templates/:id/email-rules”

Replace all email rules for a pipeline template.

Permission: recruitment:manage

Request

{
"rules": [
{
"stage_id": "string",
"template_id": "string",
"is_active": true
}
]
}

List interviews with optional status filter.

Permission: recruitment:view

Query params: status (scheduled, completed, cancelled), application_id, candidate_id

Response includes interview details with joined candidate name, job title, and participants.

Get interview details with participants.

Permission: recruitment:view

Schedule an interview. Optionally creates a Google Calendar event with attendees.

Permission: recruitment:update

Request

{
"application_id": "string (required)",
"title": "string (required)",
"interview_type": "video_call | phone | in_person | take_home",
"scheduled_at": "string (ISO datetime, required for manual)",
"duration_minutes": 60,
"location": "string",
"description": "string",
"scheduling_type": "manual | self_schedule",
"self_schedule_slots": [{ "start": "string", "end": "string" }],
"participant_ids": ["string (user IDs)"],
"create_calendar_event": true
}

Update interview details.

Permission: recruitment:update

PUT /api/recruitment/interviews/:id/cancel

Section titled “PUT /api/recruitment/interviews/:id/cancel”

Cancel an interview. Deletes the Google Calendar event if one exists.

Permission: recruitment:update

Request: { "reason": "string" }

PUT /api/recruitment/interviews/:id/complete

Section titled “PUT /api/recruitment/interviews/:id/complete”

Mark an interview as completed.

Permission: recruitment:update

PUT /api/recruitment/interviews/:id/participants/:userId/feedback

Section titled “PUT /api/recruitment/interviews/:id/participants/:userId/feedback”

Submit feedback and rating for a participant.

Permission: recruitment:update

Request

{
"feedback": "string",
"rating": 4
}

Get self-scheduling interview details and available time slots for a candidate.

Response

{
"interview": {
"id": "string",
"title": "string",
"interview_type": "string",
"duration_minutes": 60,
"description": "string | null"
},
"candidate_name": "string",
"slots": [{ "start": "string", "end": "string" }]
}

Confirm a selected time slot. Creates a Google Calendar event and updates the interview.

Request: { "start": "string (ISO datetime)" }


GET /api/recruitment/candidates/:id/comments

Section titled “GET /api/recruitment/candidates/:id/comments”

List comments for a candidate, including author name. Supports threading via parent_id.

Permission: recruitment:view

POST /api/recruitment/candidates/:id/comments

Section titled “POST /api/recruitment/candidates/:id/comments”

Add a comment to a candidate profile.

Permission: recruitment:update

Request

{
"content": "string (required)",
"application_id": "string (optional)",
"mentions": ["string (user IDs)"],
"parent_id": "string (for replies)"
}

Update a comment. Only the original author can edit.

Permission: recruitment:update

Request: { "content": "string" }

Delete a comment.

Permission: recruitment:manage


List all scorecard templates with linked stage/job names and criteria count.

Permission: recruitment:view

Response

{
"templates": [
{
"id": "string",
"name": "string",
"description": "string | null",
"pipeline_stage_id": "string | null",
"job_id": "string | null",
"is_default": 0,
"stage_name": "string | null",
"job_title": "string | null",
"criteria_count": 5,
"created_at": "string",
"updated_at": "string"
}
]
}

GET /api/recruitment/scorecard-templates/:id

Section titled “GET /api/recruitment/scorecard-templates/:id”

Get a scorecard template with all its criteria.

Permission: recruitment:view

Response

{
"template": { "..." },
"criteria": [
{
"id": "string",
"scorecard_template_id": "string",
"name": "string",
"description": "string | null",
"category": "technical | communication | culture | leadership",
"weight": 1,
"sort_order": 0,
"anchors": "{\"1\": \"Poor\", \"3\": \"Meets expectations\", \"5\": \"Exceptional\"}"
}
]
}

Create a new scorecard template with criteria.

Permission: recruitment:manage

Request

{
"name": "string (required)",
"description": "string",
"pipeline_stage_id": "string",
"job_id": "string",
"is_default": false,
"criteria": [
{
"name": "string (required)",
"description": "string",
"category": "technical",
"weight": 1,
"sort_order": 0,
"anchors": { "1": "Poor", "5": "Exceptional" }
}
]
}

PUT /api/recruitment/scorecard-templates/:id

Section titled “PUT /api/recruitment/scorecard-templates/:id”

Update a scorecard template. If criteria is provided, existing criteria are replaced entirely.

Permission: recruitment:manage

DELETE /api/recruitment/scorecard-templates/:id

Section titled “DELETE /api/recruitment/scorecard-templates/:id”

Delete a scorecard template and all its criteria.

Permission: recruitment:manage


GET /api/recruitment/applications/:id/evaluations

Section titled “GET /api/recruitment/applications/:id/evaluations”

List evaluations for an application. Implements independent scoring: if the current user has not submitted their own evaluation, other evaluators’ scores and recommendations are hidden (only names and submission status shown).

Permission: recruitment:view

Response

{
"evaluations": [
{
"id": "string",
"application_id": "string",
"evaluator_id": "string",
"evaluator_name": "string",
"evaluator_picture": "string | null",
"scorecard_template_id": "string | null",
"interview_id": "string | null",
"recommendation": "strong_yes | yes | no_decision | no | strong_no | null (hidden)",
"overall_score": "number | null (hidden)",
"is_submitted": 0,
"submitted_at": "string | null"
}
],
"can_see_scores": true,
"my_evaluation_id": "string | null"
}

Get a single evaluation with all criterion scores. Requires the requesting user to have submitted their own evaluation first (or have manage permission).

Permission: recruitment:view

POST /api/recruitment/applications/:id/evaluations

Section titled “POST /api/recruitment/applications/:id/evaluations”

Create a new evaluation draft. One evaluation per user per application per interview.

Permission: recruitment:update

Request

{
"scorecard_template_id": "string",
"interview_id": "string (optional)",
"recommendation": "strong_yes | yes | no_decision | no | strong_no",
"notes": "string",
"scores": [
{
"criteria_id": "string",
"score": 4,
"evidence": "Demonstrated strong problem-solving skills"
}
]
}

Update an evaluation. Only the evaluator (owner) can edit. Submitted evaluations cannot be modified.

Permission: recruitment:update

PUT /api/recruitment/evaluations/:id/submit

Section titled “PUT /api/recruitment/evaluations/:id/submit”

Submit an evaluation. Validates all criteria have scores, computes weighted average overall_score, sets is_submitted=1 and submitted_at. Cannot be undone.

Permission: recruitment:update

Response: { "success": true, "overall_score": 3.86 }

Delete a draft evaluation. Only the evaluator can delete, and only if not yet submitted.

Permission: recruitment:update

GET /api/recruitment/applications/:id/debrief

Section titled “GET /api/recruitment/applications/:id/debrief”

Aggregate debrief summary for all submitted evaluations. Only accessible after submitting your own evaluation (or with manage permission).

Permission: recruitment:view

Response

{
"evaluations": ["...all submitted evaluations with scores"],
"criteria_averages": [
{
"criteria_id": "string",
"criteria_name": "Technical Skills",
"criteria_category": "technical",
"average": 3.67,
"std_dev": 0.47,
"count": 3,
"min": 3,
"max": 4
}
],
"recommendation_distribution": {
"strong_yes": 1,
"yes": 2
},
"overall_average": 3.86,
"total_evaluations": 3
}

POST /api/recruitment/applications/:id/score

Section titled “POST /api/recruitment/applications/:id/score”

Score a single application using AI + rule-based matching.

Permission: recruitment:update

Response

{
"success": true,
"score": 72,
"summary": "Strong match for technical requirements..."
}

Batch score all active applications for a job.

Permission: recruitment:manage

Response

{
"success": true,
"scored": 12,
"failed": 0,
"results": [
{ "applicationId": "string", "success": true, "score": 85 }
]
}

POST /api/recruitment/candidates/:id/extract-resume

Section titled “POST /api/recruitment/candidates/:id/extract-resume”

Extract text from a candidate’s uploaded resume PDF.

Permission: recruitment:update

Response

{
"success": true,
"resume_text": "Extracted text content...",
"length": 4523
}

PUT /api/recruitment/candidates/:id/talent-pool

Section titled “PUT /api/recruitment/candidates/:id/talent-pool”

Toggle a candidate’s talent pool status.

Permission: recruitment:update

Request

{
"talent_pool": true
}

Response

{
"success": true,
"talent_pool": true
}

GET /api/recruitment/candidates/duplicates

Section titled “GET /api/recruitment/candidates/duplicates”

Find potential duplicate candidates by email or name.

Permission: recruitment:view

Query params: email, name

Response

{
"candidates": [
{
"id": "string",
"first_name": "string",
"last_name": "string",
"email": "string",
"phone": "string | null",
"source": "string",
"created_at": "string"
}
]
}

POST /api/recruitment/jobs/:id/requirements/extract

Section titled “POST /api/recruitment/jobs/:id/requirements/extract”

Use AI to extract structured requirements from a job description.

Permission: recruitment:update

Response

{
"success": true,
"requirements": [
{
"category": "required",
"description": "5+ years experience in React and TypeScript",
"priority": 4,
"weight": 1.0,
"position": 1
}
]
}

POST /api/recruitment/candidates/:id/enrich

Section titled “POST /api/recruitment/candidates/:id/enrich”

Enrich a candidate’s profile from LinkedIn via Scrapin.io. Requires linkedin_url on the candidate record and a configured Scrapin connection.

Permission: recruitment:update

Response

{
"success": true,
"profile": {
"fullName": "Jane Doe",
"headline": "Senior Software Engineer",
"currentCompany": "Acme Corp",
"currentTitle": "Senior Software Engineer",
"location": "Melbourne, Australia",
"experiences": [],
"education": [],
"skills": ["TypeScript", "React", "Node.js"],
"certifications": [],
"languages": ["English"]
}
}

GET /api/recruitment/candidates/:id/linkedin

Section titled “GET /api/recruitment/candidates/:id/linkedin”

Get stored LinkedIn enrichment data for a candidate.

Permission: recruitment:view

Response

{
"linkedin_url": "https://linkedin.com/in/janedoe",
"linkedin_data": { "...structured profile..." },
"enriched_at": "2026-03-09T10:00:00Z"
}