候補者 API
候補者の一覧・詳細・更新、インポート(TSV / アンケート)、スコアリング、選定、ステータス管理、日程確定。共通規約は API 共通仕様 を参照。
ベースパス: /api/v1/projects/{projectId}/candidates
| メソッド |
パス |
概要 |
必要ロール |
| GET |
/ |
候補者一覧取得 |
viewer |
| GET |
/fields |
フィルタ可能なフィールド一覧取得 |
viewer |
| POST |
/import/upload |
TSV ファイルアップロード |
member |
| GET |
/import/{fileId} |
アップロード済みファイル情報取得 |
viewer |
| POST |
/import/{fileId}/execute |
TSV インポート実行(非同期) |
member |
| POST |
/import/survey/preview |
アンケートインポートプレビュー |
member |
| POST |
/import/survey |
アンケートインポート実行(非同期) |
member |
| GET |
/import/{importId}/status |
インポート状況確認 |
viewer |
| GET |
/{candidateId} |
候補者詳細取得 |
viewer |
| PATCH |
/{candidateId} |
候補者更新 |
member |
| DELETE |
/{candidateId} |
候補者削除 |
member |
| POST |
/score |
スコア再計算 |
member |
| PUT |
/selection |
候補者選定タイプ一括更新 |
member |
| PATCH |
/{candidateId}/status |
候補者ステータス更新 |
member |
| GET |
/{candidateId}/history |
候補者ステータス履歴取得 |
viewer |
| GET |
/{candidateId}/email-logs |
候補者メール送信履歴取得 |
viewer |
| POST |
/{candidateId}/confirm-schedule |
日程確定(管理者) |
member |
全エンドポイントで Authorization: Bearer <access_token> が必要。更新系(POST / PATCH / PUT / DELETE)は member 以上。
candidateId は project_candidates.id
パスの {candidateId} は candidates.id ではなく project_candidates.id(プロジェクトへの参加レコードの ID)を指す。
ProjectCandidate オブジェクト
| フィールド |
型 |
説明 |
id |
integer |
プロジェクト候補者 ID |
projectId |
integer |
プロジェクト ID |
candidateId |
integer |
候補者 ID |
status |
enum |
10 値のステータス |
selectionType |
enum | null |
primary / reserve |
segmentId |
string | null |
セグメント ID |
memo |
string | null |
メモ |
priority |
string | null |
優先度(A / B / C) |
score |
string | null |
スコア(decimal の文字列表現) |
attributes |
object | null |
属性 |
surveyResponses |
object | null |
アンケート回答 |
reservationId |
integer | null |
確定予約 ID |
reservation |
object | null |
予約概要(scheduledAt / durationMinutes / interviewType / status / interviewerName) |
availableDates |
array |
候補者が送信した希望日程(id / startAt / endAt / interviewerName) |
candidate |
object |
個人情報(id / externalUserId / email / phone / name) |
createdAt / updatedAt |
string(date-time) |
作成/更新日時 |
候補者一覧取得
URI
GET /api/v1/projects/{projectId}/candidates
クエリパラメータ
| フィールド |
型 |
必須 |
既定値 |
ルール |
説明 |
page |
integer |
- |
1 |
正の整数 |
ページ番号 |
limit |
integer |
- |
20 |
1〜100 |
件数 |
sort |
string |
- |
- |
created_at / score / status / name |
並び順 |
status |
enum |
- |
- |
10 値 |
ステータスで絞り込み |
selectionType |
enum |
- |
- |
primary / reserve |
選定区分で絞り込み |
segmentId |
string |
- |
- |
- |
セグメントで絞り込み |
minScore |
number |
- |
- |
- |
スコア下限 |
maxScore |
number |
- |
- |
- |
スコア上限 |
search |
string |
- |
- |
- |
氏名またはメールの部分一致 |
responseFilter |
string |
- |
- |
JSON 文字列 |
回答内容による絞り込み |
responseFilter の指定方法
attributes と surveyResponses のフィールドを条件に絞り込む。JSON を文字列化して渡す。
単純な条件の配列:
[
{ "field": "性別", "operator": "eq", "value": "女性" },
{ "field": "attributes.prefecture", "operator": "in", "value": ["東京", "神奈川"] }
]
論理演算子を指定する形式:
{
"conditions": [
{ "field": "性別", "operator": "eq", "value": "女性" },
{
"conditions": [
{ "field": "年代", "operator": "eq", "value": "20代" },
{ "field": "年代", "operator": "eq", "value": "30代" }
],
"logic": "or"
}
],
"logic": "and"
}
| フィールド |
ルール |
field |
必須。attributes または surveyResponses のフィールド名 |
operator |
必須。eq / neq / contains / in |
value |
必須。文字列・数値、または配列(in の場合) |
logic |
任意。and(既定)/ or |
レスポンス(200 OK)
data が ProjectCandidate の配列、pagination 付き。
例外処理
| 説明 |
ステータスコード |
ステータス名 |
クエリまたは responseFilter の形式不正 |
400 |
Bad Request |
| トークンが欠落または無効 |
401 |
Unauthorized |
| プロジェクトが存在しない |
404 |
Not Found |
| サーバ内部エラー |
500 |
Internal Server Error |
フィルタ可能なフィールド一覧取得
登録済み候補者の attributes / surveyResponses に実在するフィールドと、そのサンプル値を返す。セグメント定義や絞り込み UI の選択肢を作るために使う。
URI
GET /api/v1/projects/{projectId}/candidates/fields
レスポンス(200 OK)
{
"success": true,
"status": "success",
"statusCode": 200,
"data": {
"fields": [
{ "name": "prefecture", "source": "attributes", "sampleValues": ["東京", "大阪", "福岡"] },
{ "name": "性別", "source": "surveyResponses", "sampleValues": ["男性", "女性"] }
]
},
"path": "/api/v1/projects/1/candidates/fields",
"method": "GET"
}
| フィールド |
型 |
説明 |
name |
string |
フィールド名 |
source |
enum |
attributes / surveyResponses |
sampleValues |
string[] |
サンプル値 |
例外処理
| 説明 |
ステータスコード |
ステータス名 |
| トークンが欠落または無効 |
401 |
Unauthorized |
| プロジェクトが存在しない |
404 |
Not Found |
| サーバ内部エラー |
500 |
Internal Server Error |
TSV ファイルアップロード
URI
POST /api/v1/projects/{projectId}/candidates/import/upload
リクエストボディ
multipart/form-data。
| フィールド |
必須 |
ルール |
file |
◯ |
TSV ファイル(UTF-16LE、タブ区切り) |
レスポンス(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": "回答者ID", "sampleValues": ["1001", "1002"] },
{ "index": 1, "name": "お名前", "sampleValues": ["山田太郎", "佐藤花子"] }
],
"suggested_mapping": { "external_user_id": { "column_index": 0 } },
"preview": [{ "回答者ID": "1001", "お名前": "山田太郎" }],
"expires_at": "2026-01-15T11:00:00Z"
},
"path": "/api/v1/projects/1/candidates/import/upload",
"method": "POST"
}
| フィールド |
型 |
説明 |
file_id |
string(uuid) |
一時ファイル ID。実行時に指定する |
file_info |
object |
ファイル名・サイズ・行数 |
columns |
array |
列インデックス・列名・サンプル値 |
suggested_mapping |
object |
列名から推測したマッピング案 |
preview |
array |
先頭数行のプレビュー |
expires_at |
string(date-time) |
一時ファイルの有効期限 |
例外処理
| 説明 |
ステータスコード |
ステータス名 |
| ファイル未添付・パース失敗 |
400 |
Bad Request |
| トークンが欠落または無効 |
401 |
Unauthorized |
| ロール不足 |
403 |
Forbidden |
| プロジェクトが存在しない |
404 |
Not Found |
| サーバ内部エラー |
500 |
Internal Server Error |
アップロード済みファイル情報取得
アップロード直後のレスポンスと同じ内容を再取得する。画面リロード後にマッピング作業を再開するために使う。
URI
GET /api/v1/projects/{projectId}/candidates/import/{fileId}
パスパラメータ
レスポンス(200 OK)
アップロード時と同じ ImportUploadResponse。
例外処理
| 説明 |
ステータスコード |
ステータス名 |
| トークンが欠落または無効 |
401 |
Unauthorized |
| ファイルが存在しない/期限切れ |
404 |
Not Found |
| サーバ内部エラー |
500 |
Internal Server Error |
TSV インポート実行
非同期で実行され、受付時点で 201 を返す。進捗は「インポート状況確認」で追う。
URI
POST /api/v1/projects/{projectId}/candidates/import/{fileId}/execute
リクエストボディ
{
"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]
}
}
}
バリデーションルール
| フィールド |
ルール |
mapping.external_user_id.column_index |
必須。0 以上の整数 |
mapping.name.column_index |
必須。0 以上の整数 |
mapping.email.column_index |
必須。0 以上の整数 |
mapping.phone.column_index |
任意。0 以上の整数 |
mapping.attributes |
任意。フィールド名 → マッピング定義 |
mapping.survey_responses.mode |
任意。all_remaining / specified |
mapping.survey_responses.exclude_columns |
任意。列インデックスの配列 |
mapping.survey_responses.include_columns |
任意。列インデックスの配列 |
attributes のマッピング定義は 2 種類:
| type |
フィールド |
説明 |
direct |
column_index |
指定列の値をそのまま使う |
single_select |
columns / mapping |
複数列の選択肢群から 1 値に畳む。mapping は値の対応表または "first_selected" |
レスポンス(201 Created)
{
"success": true,
"status": "success",
"statusCode": 201,
"data": { "importLogId": 12, "status": "processing" },
"path": "/api/v1/projects/1/candidates/import/550e.../execute",
"method": "POST"
}
例外処理
| 説明 |
ステータスコード |
ステータス名 |
| マッピングの形式不正 |
400 |
Bad Request |
| トークンが欠落または無効 |
401 |
Unauthorized |
| ロール不足 |
403 |
Forbidden |
| ファイルまたはプロジェクトが存在しない |
404 |
Not Found |
| サーバ内部エラー |
500 |
Internal Server Error |
アンケートインポートプレビュー
実際には登録せず、マッピングとフィルタを適用した結果を試算する。
URI
POST /api/v1/projects/{projectId}/candidates/import/survey/preview
リクエストボディ
{
"surveyId": "234567",
"mapping": {
"externalUserId": { "type": "direct", "sourceField": "回答者ID" },
"name": { "type": "direct", "sourceField": "お名前をご記入ください" },
"email": { "type": "direct", "sourceField": "メールアドレス" }
},
"filter": {
"conditions": [{ "field": "回答状況", "operator": "equals", "value": "完了" }],
"logic": "and"
},
"sampleSize": 10
}
バリデーションルール
| フィールド |
ルール |
surveyId |
必須。1 文字以上 |
mapping |
必須。ImportMapping(プロジェクト API を参照) |
filter |
任意。ImportFilter |
sampleSize |
任意。1〜100 の整数 |
レスポンス(200 OK)
{
"success": true,
"status": "success",
"statusCode": 200,
"data": {
"surveyId": "234567",
"totalRespondents": 1234,
"filteredRespondents": 320,
"sampleData": [
{ "externalUserId": "1001", "name": "山田太郎", "email": "yamada@example.com" }
],
"warnings": ["メールアドレスが空の回答が 3 件あります"]
},
"path": "/api/v1/projects/1/candidates/import/survey/preview",
"method": "POST"
}
| フィールド |
型 |
説明 |
totalRespondents |
integer |
フィルタ前の回答者数 |
filteredRespondents |
integer |
フィルタ後に取り込まれる見込みの件数 |
sampleData |
array |
マッピング適用後のサンプル |
warnings |
string[] |
警告メッセージ |
例外処理
| 説明 |
ステータスコード |
ステータス名 |
| バリデーションエラー |
400 |
Bad Request |
| トークンが欠落または無効 |
401 |
Unauthorized |
| ロール不足 |
403 |
Forbidden |
| プロジェクトまたはアンケートが存在しない |
404 |
Not Found |
| Databricks 未設定 |
503 |
Service Unavailable |
アンケートインポート実行
インポートログを先に作成し、ECS タスク(本番)またはサブプロセス(開発)へ処理を委譲して即座に返す。
URI
POST /api/v1/projects/{projectId}/candidates/import/survey
リクエストボディ
プレビューから sampleSize を除いたもの。
{
"surveyId": "234567",
"mapping": { "externalUserId": { "type": "direct", "sourceField": "回答者ID" } },
"filter": { "conditions": [], "logic": "and" }
}
レスポンス(201 Created)
受付時点の値が返るため、件数はすべて 0。
{
"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"
}
例外処理
| 説明 |
ステータスコード |
ステータス名 |
| バリデーションエラー |
400 |
Bad Request |
| トークンが欠落または無効 |
401 |
Unauthorized |
| ロール不足 |
403 |
Forbidden |
| プロジェクトが存在しない |
404 |
Not Found |
処理フロー
sequenceDiagram
participant FE as フロントエンド
participant API as Backend API
participant DB as PostgreSQL
participant Task as ECS タスク / サブプロセス
FE->>API: POST /candidates/import/survey
API->>DB: import_logs を作成(pending)
API->>Task: インポート処理を起動
API-->>FE: 201(importLogId, processing)
Task->>DB: Databricks から回答を読み、候補者を登録
Task->>DB: import_logs を completed / partial / failed に更新
loop ポーリング
FE->>API: GET /candidates/import/{importId}/status
API-->>FE: 進捗と件数
end
インポート状況確認
TSV / アンケートいずれのインポートにも使える。
URI
GET /api/v1/projects/{projectId}/candidates/import/{importId}/status
パスパラメータ
| フィールド |
ルール |
importId |
必須。正の整数(import_logs.id) |
レスポンス(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": "メールアドレスが不正です" }
]
},
"path": "/api/v1/projects/1/candidates/import/12/status",
"method": "GET"
}
status |
意味 |
pending |
受付済み・未開始 |
processing |
処理中 |
completed |
全件成功 |
partial |
一部成功 |
failed |
失敗 |
例外処理
| 説明 |
ステータスコード |
ステータス名 |
| トークンが欠落または無効 |
401 |
Unauthorized |
| インポートログが存在しない |
404 |
Not Found |
| サーバ内部エラー |
500 |
Internal Server Error |
候補者詳細取得
URI
GET /api/v1/projects/{projectId}/candidates/{candidateId}
レスポンス(200 OK)
data に ProjectCandidate。reservation と availableDates を含む。
例外処理
| 説明 |
ステータスコード |
ステータス名 |
| トークンが欠落または無効 |
401 |
Unauthorized |
| 候補者が存在しない |
404 |
Not Found |
| サーバ内部エラー |
500 |
Internal Server Error |
候補者更新
URI
PATCH /api/v1/projects/{projectId}/candidates/{candidateId}
リクエストボディ
{
"attributes": { "prefecture": "東京" },
"surveyResponses": { "性別": "女性" },
"memo": "要フォロー",
"priority": "A"
}
バリデーションルール
| フィールド |
ルール |
attributes |
任意。任意構造のオブジェクト |
surveyResponses |
任意。任意構造のオブジェクト |
memo |
任意。文字列または null |
priority |
任意。文字列または null(DB 上は 1 文字) |
個人情報は更新できない
氏名・メールアドレス・電話番号は candidates テーブル側の情報であり、このエンドポイントでは変更できない。
レスポンス(200 OK)
data に更新後の ProjectCandidate。
例外処理
| 説明 |
ステータスコード |
ステータス名 |
| バリデーションエラー |
400 |
Bad Request |
| トークンが欠落または無効 |
401 |
Unauthorized |
| ロール不足 |
403 |
Forbidden |
| 候補者が存在しない |
404 |
Not Found |
候補者削除
URI
DELETE /api/v1/projects/{projectId}/candidates/{candidateId}
レスポンス(204 No Content)
ボディなし。
例外処理
| 説明 |
ステータスコード |
ステータス名 |
| トークンが欠落または無効 |
401 |
Unauthorized |
| ロール不足 |
403 |
Forbidden |
| 候補者が存在しない |
404 |
Not Found |
Warning
プロジェクトへの参加(project_candidates)が削除され、メールログ・調整トークン・希望日程・予約・ステータス履歴も連鎖削除される。個人(candidates)自体は残る。
スコア再計算
プロジェクトの scoring_rules を全候補者に再適用する。
URI
POST /api/v1/projects/{projectId}/candidates/score
リクエスト
ボディなし。
レスポンス(200 OK)
{
"success": true,
"status": "success",
"statusCode": 200,
"data": { "updated": 25 },
"path": "/api/v1/projects/1/candidates/score",
"method": "POST"
}
| フィールド |
型 |
説明 |
updated |
integer |
スコアが更新された候補者数 |
例外処理
| 説明 |
ステータスコード |
ステータス名 |
スコアリングルールが不正(SCORING_ERROR) |
400 |
Bad Request |
| トークンが欠落または無効 |
401 |
Unauthorized |
| ロール不足 |
403 |
Forbidden |
| プロジェクトが存在しない |
404 |
Not Found |
候補者選定タイプ一括更新
STEP2 の選定結果を保存する。
URI
PUT /api/v1/projects/{projectId}/candidates/selection
リクエストボディ
{
"selections": [
{ "candidateId": 12, "selectionType": "primary", "segmentId": "seg-1" },
{ "candidateId": 34, "selectionType": "reserve", "segmentId": "seg-1" },
{ "candidateId": 56, "selectionType": null, "segmentId": null }
]
}
バリデーションルール
| フィールド |
ルール |
selections |
必須。配列 |
selections[].candidateId |
必須。整数(project_candidates.id) |
selections[].selectionType |
必須。primary / reserve / null(選定解除) |
selections[].segmentId |
任意。文字列または null |
レスポンス(200 OK)
{
"success": true,
"status": "success",
"statusCode": 200,
"data": { "updated": 3 },
"path": "/api/v1/projects/1/candidates/selection",
"method": "PUT"
}
例外処理
| 説明 |
ステータスコード |
ステータス名 |
| バリデーションエラー |
400 |
Bad Request |
| トークンが欠落または無効 |
401 |
Unauthorized |
| ロール不足 |
403 |
Forbidden |
| プロジェクトが存在しない |
404 |
Not Found |
候補者ステータス更新
URI
PATCH /api/v1/projects/{projectId}/candidates/{candidateId}/status
リクエストボディ
{
"status": "contacted",
"note": "案内メールを送信"
}
バリデーションルール
| フィールド |
ルール |
status |
必須。not_contacted / contacted / waiting_response / scheduling / scheduled / interviewed / completed / declined / bounced / cancelled |
note |
任意。備考 |
遷移ルール
| 現在 |
遷移可能な次の状態 |
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 |
(なし) |
declined |
(なし) |
bounced |
contacted |
cancelled |
not_contacted |
レスポンス(200 OK)
data に更新後の ProjectCandidate。同時に status_histories に 1 行追加される。
例外処理
| 説明 |
ステータスコード |
ステータス名 |
許可されていない遷移(INVALID_STATUS_TRANSITION) |
400 |
Bad Request |
| トークンが欠落または無効 |
401 |
Unauthorized |
| ロール不足 |
403 |
Forbidden |
| 候補者が存在しない |
404 |
Not Found |
候補者ステータス履歴取得
URI
GET /api/v1/projects/{projectId}/candidates/{candidateId}/history
レスポンス(200 OK)
data が StatusHistory の配列(ページネーションなし)。
{
"success": true,
"status": "success",
"statusCode": 200,
"data": [
{
"id": 1,
"projectCandidateId": 12,
"oldStatus": "not_contacted",
"newStatus": "contacted",
"changedBy": 1,
"note": "案内メールを送信",
"createdAt": "2026-01-15T09:00:00Z"
}
],
"path": "/api/v1/projects/1/candidates/12/history",
"method": "GET"
}
例外処理
| 説明 |
ステータスコード |
ステータス名 |
| トークンが欠落または無効 |
401 |
Unauthorized |
| 候補者が存在しない |
404 |
Not Found |
候補者メール送信履歴取得
URI
GET /api/v1/projects/{projectId}/candidates/{candidateId}/email-logs
レスポンス(200 OK)
data が EmailLog の配列(ページネーションなし)。フィールドは メール API を参照。
例外処理
| 説明 |
ステータスコード |
ステータス名 |
| トークンが欠落または無効 |
401 |
Unauthorized |
| 候補者が存在しない |
404 |
Not Found |
日程確定(管理者)
候補者が送信した希望日程から 1 件を選んで予約を作成する。
URI
POST /api/v1/projects/{projectId}/candidates/{candidateId}/confirm-schedule
リクエストボディ
バリデーションルール
| フィールド |
ルール |
availableDateId |
必須。正の整数(candidate_available_dates.id)。対象候補者のものである必要がある |
レスポンス(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"
}
例外処理
| 説明 |
ステータスコード |
ステータス名 |
| バリデーションエラー |
400 |
Bad Request |
| トークンが欠落または無効 |
401 |
Unauthorized |
| ロール不足 |
403 |
Forbidden |
| 候補者または希望日程が存在しない |
404 |
Not Found |
既に確定済み(SLOT_NOT_AVAILABLE) |
409 |
Conflict |
処理内容
candidate_available_dates から対象を取得し、候補者との一致を検証する
- 既に
confirmed の予約があれば SLOT_NOT_AVAILABLE で中断する
reservations を作成する
- 面接官の Google Calendar 上の仮枠を確定イベントに変換する
- 候補者へ確定通知メールを送る
PROTOTYPE_MODE
PROTOTYPE_MODE=true の間は 4. と 5. がスキップされ、DB の予約作成のみが行われる。