Skip to content

Candidates API

Listing, viewing and updating candidates, imports (TSV / survey), scoring, selection, status management, and schedule confirmation. See API Conventions for shared rules.

Base path: /api/v1/projects/{projectId}/candidates

Method Path Summary Role
GET / List candidates viewer
GET /fields List filterable fields viewer
POST /import/upload Upload a TSV file member
GET /import/{fileId} Get uploaded file info viewer
POST /import/{fileId}/execute Execute TSV import (async) member
POST /import/survey/preview Preview a survey import member
POST /import/survey Execute a survey import (async) member
GET /import/{importId}/status Check import status viewer
GET /{candidateId} Get candidate detail viewer
PATCH /{candidateId} Update a candidate member
DELETE /{candidateId} Delete a candidate member
POST /score Recalculate scores member
PUT /selection Bulk update selection types member
PATCH /{candidateId}/status Update candidate status member
GET /{candidateId}/history Get status history viewer
GET /{candidateId}/email-logs Get email history viewer
POST /{candidateId}/confirm-schedule Confirm the schedule (admin) member

All endpoints require Authorization: Bearer <access_token>. Writes (POST / PATCH / PUT / DELETE) require member or above.

candidateId is project_candidates.id

The {candidateId} path segment refers to project_candidates.id (the participation record), not candidates.id.

The ProjectCandidate Object

Field Type Description
id integer Project candidate ID
projectId integer Project ID
candidateId integer Candidate ID
status enum One of ten statuses
selectionType enum | null primary / reserve
segmentId string | null Segment ID
memo string | null Memo
priority string | null Priority (A / B / C)
score string | null Score (decimal rendered as a string)
attributes object | null Attributes
surveyResponses object | null Survey answers
reservationId integer | null Confirmed reservation ID
reservation object | null Reservation summary (scheduledAt / durationMinutes / interviewType / status / interviewerName)
availableDates array Slots submitted by the candidate (id / startAt / endAt / interviewerName)
candidate object Personal data (id / externalUserId / email / phone / name)
createdAt / updatedAt string(date-time) Creation / update timestamps

List Candidates

URI

GET /api/v1/projects/{projectId}/candidates

Query Parameters

Field Type Required Default Rules Description
page integer - 1 Positive integer Page number
limit integer - 20 1–100 Items per page
sort string - - created_at / score / status / name Sort order
status enum - - Ten values Filter by status
selectionType enum - - primary / reserve Filter by selection type
segmentId string - - - Filter by segment
minScore number - - - Minimum score
maxScore number - - - Maximum score
search string - - - Partial match on name or email
responseFilter string - - JSON string Filter by answer content

Using responseFilter

Filters on fields inside attributes and surveyResponses. Pass the JSON as a string.

An array of simple conditions:

[
  { "field": "Gender", "operator": "eq", "value": "Female" },
  { "field": "attributes.prefecture", "operator": "in", "value": ["Tokyo", "Kanagawa"] }
]

Or with an explicit logical operator:

{
  "conditions": [
    { "field": "Gender", "operator": "eq", "value": "Female" },
    {
      "conditions": [
        { "field": "Age group", "operator": "eq", "value": "20s" },
        { "field": "Age group", "operator": "eq", "value": "30s" }
      ],
      "logic": "or"
    }
  ],
  "logic": "and"
}
Field Rules
field Required; a field name in attributes or surveyResponses
operator Required; eq / neq / contains / in
value Required; string, number, or array (for in)
logic Optional; and (default) / or

Response (200 OK)

data is an array of ProjectCandidate, with pagination.

Errors

Description Status code Status name
Malformed query or responseFilter 400 Bad Request
Token missing or invalid 401 Unauthorized
Project does not exist 404 Not Found
Server internal error 500 Internal Server Error

List Filterable Fields

Returns the fields actually present in registered candidates' attributes / surveyResponses, along with sample values. Used to build segment definitions and filter UI options.

URI

GET /api/v1/projects/{projectId}/candidates/fields

Response (200 OK)

{
  "success": true,
  "status": "success",
  "statusCode": 200,
  "data": {
    "fields": [
      { "name": "prefecture", "source": "attributes", "sampleValues": ["Tokyo", "Osaka", "Fukuoka"] },
      { "name": "Gender", "source": "surveyResponses", "sampleValues": ["Male", "Female"] }
    ]
  },
  "path": "/api/v1/projects/1/candidates/fields",
  "method": "GET"
}
Field Type Description
name string Field name
source enum attributes / surveyResponses
sampleValues string[] Sample values

Errors

Description Status code Status name
Token missing or invalid 401 Unauthorized
Project does not exist 404 Not Found
Server internal error 500 Internal Server Error

Upload TSV File

URI

POST /api/v1/projects/{projectId}/candidates/import/upload

Request Body

multipart/form-data.

Field Required Rules
file ✓ TSV file (UTF-16LE, tab separated)

Response (200 OK)

{
  "success": true,
  "status": "success",
  "statusCode": 200,
  "data": {
    "file_id": "550e8400-e29b-41d4-a716-446655440000",
    "file_info": { "name": "candidates.tsv", "size": 20480, "rowCount": 120 },
    "columns": [
      { "index": 0, "name": "Respondent ID", "sampleValues": ["1001", "1002"] },
      { "index": 1, "name": "Name", "sampleValues": ["Taro Yamada", "Hanako Sato"] }
    ],
    "suggested_mapping": { "external_user_id": { "column_index": 0 } },
    "preview": [{ "Respondent ID": "1001", "Name": "Taro Yamada" }],
    "expires_at": "2026-01-15T11:00:00Z"
  },
  "path": "/api/v1/projects/1/candidates/import/upload",
  "method": "POST"
}
Field Type Description
file_id string(uuid) Temporary file ID, passed to the execute call
file_info object Name, size, row count
columns array Column index, name, sample values
suggested_mapping object Mapping guessed from column names
preview array Preview of the first rows
expires_at string(date-time) Expiry of the temporary file

Errors

Description Status code Status name
No file attached or parse failure 400 Bad Request
Token missing or invalid 401 Unauthorized
Insufficient role 403 Forbidden
Project does not exist 404 Not Found
Server internal error 500 Internal Server Error

Get Uploaded File Info

Re-fetches the same payload the upload returned, so mapping work can resume after a page reload.

URI

GET /api/v1/projects/{projectId}/candidates/import/{fileId}

Path Parameters

Field Rules
fileId Required UUID

Response (200 OK)

The same ImportUploadResponse as the upload.

Errors

Description Status code Status name
Token missing or invalid 401 Unauthorized
File does not exist or has expired 404 Not Found
Server internal error 500 Internal Server Error

Execute TSV Import

Runs asynchronously and returns 201 as soon as the request is accepted. Track progress via "Check Import Status".

URI

POST /api/v1/projects/{projectId}/candidates/import/{fileId}/execute

Request Body

{
  "mapping": {
    "external_user_id": { "column_index": 0 },
    "name": { "column_index": 1 },
    "email": { "column_index": 2 },
    "phone": { "column_index": 3 },
    "attributes": {
      "prefecture": { "type": "direct", "column_index": 4 },
      "gender": { "type": "single_select", "columns": [5, 6], "mapping": "first_selected" }
    },
    "survey_responses": {
      "mode": "all_remaining",
      "exclude_columns": [2, 3]
    }
  }
}

Validation Rules

Field Rules
mapping.external_user_id.column_index Required integer ≥ 0
mapping.name.column_index Required integer ≥ 0
mapping.email.column_index Required integer ≥ 0
mapping.phone.column_index Optional integer ≥ 0
mapping.attributes Optional; field name → mapping definition
mapping.survey_responses.mode Optional; all_remaining / specified
mapping.survey_responses.exclude_columns Optional array of column indexes
mapping.survey_responses.include_columns Optional array of column indexes

Attribute mappings come in two shapes:

type Fields Description
direct column_index Use the value of that column as-is
single_select columns / mapping Collapse several option columns into one value; mapping is a lookup table or "first_selected"

Response (201 Created)

{
  "success": true,
  "status": "success",
  "statusCode": 201,
  "data": { "importLogId": 12, "status": "processing" },
  "path": "/api/v1/projects/1/candidates/import/550e.../execute",
  "method": "POST"
}

Errors

Description Status code Status name
Malformed mapping 400 Bad Request
Token missing or invalid 401 Unauthorized
Insufficient role 403 Forbidden
File or project does not exist 404 Not Found
Server internal error 500 Internal Server Error

Preview Survey Import

Dry-runs the mapping and filter without writing anything.

URI

POST /api/v1/projects/{projectId}/candidates/import/survey/preview

Request Body

{
  "surveyId": "234567",
  "mapping": {
    "externalUserId": { "type": "direct", "sourceField": "Respondent ID" },
    "name": { "type": "direct", "sourceField": "Please enter your name" },
    "email": { "type": "direct", "sourceField": "Email address" }
  },
  "filter": {
    "conditions": [{ "field": "Response state", "operator": "equals", "value": "Completed" }],
    "logic": "and"
  },
  "sampleSize": 10
}

Validation Rules

Field Rules
surveyId Required, at least 1 character
mapping Required ImportMapping (see Projects API)
filter Optional ImportFilter
sampleSize Optional integer 1–100

Response (200 OK)

{
  "success": true,
  "status": "success",
  "statusCode": 200,
  "data": {
    "surveyId": "234567",
    "totalRespondents": 1234,
    "filteredRespondents": 320,
    "sampleData": [
      { "externalUserId": "1001", "name": "Taro Yamada", "email": "yamada@example.com" }
    ],
    "warnings": ["3 responses have an empty email address"]
  },
  "path": "/api/v1/projects/1/candidates/import/survey/preview",
  "method": "POST"
}
Field Type Description
totalRespondents integer Respondents before filtering
filteredRespondents integer Expected number of rows to import
sampleData array Sample rows after the mapping is applied
warnings string[] Warning messages

Errors

Description Status code Status name
Validation error 400 Bad Request
Token missing or invalid 401 Unauthorized
Insufficient role 403 Forbidden
Project or survey does not exist 404 Not Found
Databricks not configured 503 Service Unavailable

Execute Survey Import

Creates the import log first, hands the work to an ECS task (production) or a subprocess (development), and returns immediately.

URI

POST /api/v1/projects/{projectId}/candidates/import/survey

Request Body

The preview body minus sampleSize.

{
  "surveyId": "234567",
  "mapping": { "externalUserId": { "type": "direct", "sourceField": "Respondent ID" } },
  "filter": { "conditions": [], "logic": "and" }
}

Response (201 Created)

The values reflect acceptance time, so all counts are zero.

{
  "success": true,
  "status": "success",
  "statusCode": 201,
  "data": {
    "importLogId": 12,
    "status": "processing",
    "importedCount": 0,
    "skippedCount": 0,
    "errorCount": 0,
    "duplicateCount": 0,
    "errors": []
  },
  "path": "/api/v1/projects/1/candidates/import/survey",
  "method": "POST"
}

Errors

Description Status code Status name
Validation error 400 Bad Request
Token missing or invalid 401 Unauthorized
Insufficient role 403 Forbidden
Project does not exist 404 Not Found

Flow

sequenceDiagram
    participant FE as Frontend
    participant API as Backend API
    participant DB as PostgreSQL
    participant Task as ECS task / subprocess

    FE->>API: POST /candidates/import/survey
    API->>DB: Create import_logs (pending)
    API->>Task: Launch the import job
    API-->>FE: 201 (importLogId, processing)
    Task->>DB: Read answers from Databricks and register candidates
    Task->>DB: Update import_logs to completed / partial / failed
    loop Polling
        FE->>API: GET /candidates/import/{importId}/status
        API-->>FE: Progress and counts
    end

Check Import Status

Works for both TSV and survey imports.

URI

GET /api/v1/projects/{projectId}/candidates/import/{importId}/status

Path Parameters

Field Rules
importId Required positive integer (import_logs.id)

Response (200 OK)

{
  "success": true,
  "status": "success",
  "statusCode": 200,
  "data": {
    "importLogId": 12,
    "status": "completed",
    "importedCount": 300,
    "skippedCount": 15,
    "errorCount": 2,
    "duplicateCount": 3,
    "errors": [
      { "row": 42, "respondentId": "1042", "field": "email", "message": "Invalid email address" }
    ]
  },
  "path": "/api/v1/projects/1/candidates/import/12/status",
  "method": "GET"
}
status Meaning
pending Accepted, not started
processing In progress
completed All rows succeeded
partial Partially succeeded
failed Failed

Errors

Description Status code Status name
Token missing or invalid 401 Unauthorized
Import log does not exist 404 Not Found
Server internal error 500 Internal Server Error

Get Candidate Detail

URI

GET /api/v1/projects/{projectId}/candidates/{candidateId}

Response (200 OK)

data holds a ProjectCandidate, including reservation and availableDates.

Errors

Description Status code Status name
Token missing or invalid 401 Unauthorized
Candidate does not exist 404 Not Found
Server internal error 500 Internal Server Error

Update Candidate

URI

PATCH /api/v1/projects/{projectId}/candidates/{candidateId}

Request Body

{
  "attributes": { "prefecture": "Tokyo" },
  "surveyResponses": { "Gender": "Female" },
  "memo": "Needs follow-up",
  "priority": "A"
}

Validation Rules

Field Rules
attributes Optional object of arbitrary shape
surveyResponses Optional object of arbitrary shape
memo Optional string or null
priority Optional string or null (one character in the DB)

Personal data cannot be updated

Name, email and phone belong to the candidates table and cannot be changed through this endpoint.

Response (200 OK)

data holds the updated ProjectCandidate.

Errors

Description Status code Status name
Validation error 400 Bad Request
Token missing or invalid 401 Unauthorized
Insufficient role 403 Forbidden
Candidate does not exist 404 Not Found

Delete Candidate

URI

DELETE /api/v1/projects/{projectId}/candidates/{candidateId}

Response (204 No Content)

No body.

Errors

Description Status code Status name
Token missing or invalid 401 Unauthorized
Insufficient role 403 Forbidden
Candidate does not exist 404 Not Found

Warning

The participation row (project_candidates) is deleted along with its email logs, scheduling tokens, preferred slots, reservation and status history. The person (candidates) remains.


Recalculate Scores

Re-applies the project's scoring_rules to every candidate.

URI

POST /api/v1/projects/{projectId}/candidates/score

Request

No body.

Response (200 OK)

{
  "success": true,
  "status": "success",
  "statusCode": 200,
  "data": { "updated": 25 },
  "path": "/api/v1/projects/1/candidates/score",
  "method": "POST"
}
Field Type Description
updated integer Number of candidates whose score changed

Errors

Description Status code Status name
Invalid scoring rules (SCORING_ERROR) 400 Bad Request
Token missing or invalid 401 Unauthorized
Insufficient role 403 Forbidden
Project does not exist 404 Not Found

Bulk Update Selection Types

Persists the results of STEP2.

URI

PUT /api/v1/projects/{projectId}/candidates/selection

Request Body

{
  "selections": [
    { "candidateId": 12, "selectionType": "primary", "segmentId": "seg-1" },
    { "candidateId": 34, "selectionType": "reserve", "segmentId": "seg-1" },
    { "candidateId": 56, "selectionType": null, "segmentId": null }
  ]
}

Validation Rules

Field Rules
selections Required array
selections[].candidateId Required integer (project_candidates.id)
selections[].selectionType Required; primary / reserve / null (clears the selection)
selections[].segmentId Optional string or null

Response (200 OK)

{
  "success": true,
  "status": "success",
  "statusCode": 200,
  "data": { "updated": 3 },
  "path": "/api/v1/projects/1/candidates/selection",
  "method": "PUT"
}

Errors

Description Status code Status name
Validation error 400 Bad Request
Token missing or invalid 401 Unauthorized
Insufficient role 403 Forbidden
Project does not exist 404 Not Found

Update Candidate Status

URI

PATCH /api/v1/projects/{projectId}/candidates/{candidateId}/status

Request Body

{
  "status": "contacted",
  "note": "Invitation sent"
}

Validation Rules

Field Rules
status Required; not_contacted / contacted / waiting_response / scheduling / scheduled / interviewed / completed / declined / bounced / cancelled
note Optional note

Transition Rules

Current Allowed next states
not_contacted contacted / declined / cancelled
contacted waiting_response / bounced / declined / cancelled
waiting_response scheduling / declined / cancelled
scheduling scheduled / declined / cancelled
scheduled interviewed / cancelled
interviewed completed / cancelled
completed (none)
declined (none)
bounced contacted
cancelled not_contacted

Response (200 OK)

data holds the updated ProjectCandidate, and one row is appended to status_histories.

Errors

Description Status code Status name
Disallowed transition (INVALID_STATUS_TRANSITION) 400 Bad Request
Token missing or invalid 401 Unauthorized
Insufficient role 403 Forbidden
Candidate does not exist 404 Not Found

Get Status History

URI

GET /api/v1/projects/{projectId}/candidates/{candidateId}/history

Response (200 OK)

data is an array of StatusHistory (not paginated).

{
  "success": true,
  "status": "success",
  "statusCode": 200,
  "data": [
    {
      "id": 1,
      "projectCandidateId": 12,
      "oldStatus": "not_contacted",
      "newStatus": "contacted",
      "changedBy": 1,
      "note": "Invitation sent",
      "createdAt": "2026-01-15T09:00:00Z"
    }
  ],
  "path": "/api/v1/projects/1/candidates/12/history",
  "method": "GET"
}

Errors

Description Status code Status name
Token missing or invalid 401 Unauthorized
Candidate does not exist 404 Not Found

Get Candidate Email History

URI

GET /api/v1/projects/{projectId}/candidates/{candidateId}/email-logs

Response (200 OK)

data is an array of EmailLog (not paginated). See the Emails API for the field list.

Errors

Description Status code Status name
Token missing or invalid 401 Unauthorized
Candidate does not exist 404 Not Found

Confirm Schedule (Admin)

Picks one of the candidate's submitted slots and creates the reservation.

URI

POST /api/v1/projects/{projectId}/candidates/{candidateId}/confirm-schedule

Request Body

{
  "availableDateId": 5
}

Validation Rules

Field Rules
availableDateId Required positive integer (candidate_available_dates.id); must belong to this candidate

Response (201 Created)

{
  "success": true,
  "status": "success",
  "statusCode": 201,
  "data": {
    "message": "日程を確定しました",
    "reservationId": 7,
    "scheduledAt": "2026-02-03T01:00:00.000Z",
    "durationMinutes": 60
  },
  "path": "/api/v1/projects/1/candidates/12/confirm-schedule",
  "method": "POST"
}

Errors

Description Status code Status name
Validation error 400 Bad Request
Token missing or invalid 401 Unauthorized
Insufficient role 403 Forbidden
Candidate or preferred slot does not exist 404 Not Found
Already confirmed (SLOT_NOT_AVAILABLE) 409 Conflict

What Happens

  1. Fetch the row from candidate_available_dates and verify it belongs to the candidate
  2. Abort with SLOT_NOT_AVAILABLE if a confirmed reservation already exists
  3. Create the reservations row
  4. Convert the tentative event on the interviewer's Google Calendar into a confirmed one
  5. Send the confirmation email to the candidate

PROTOTYPE_MODE

While PROTOTYPE_MODE=true, steps 4 and 5 are skipped and only the reservation row is created.