日程調整 API(公開)
候補者向けの公開エンドポイント。認証を必要とせず、発行された調整トークンのみでアクセスする。共通規約は API 共通仕様 を参照。
ベースパス: /api/v1/scheduling
| メソッド | パス | 概要 | 認証 |
|---|---|---|---|
| GET | /{token} |
スケジュール情報取得 | 不要 |
| GET | /{token}/slots |
予約可能枠取得 | 不要 |
| POST | /{token}/submit-availability |
希望日程送信 | 不要 |
対応する画面はフロントエンドの /scheduling/{token}(src/app/scheduling/[token]/)。
レスポンス形式が他と異なる
このルータだけは共通の封筒形式(success / statusCode / timestamp / path / method)を返さない。{ "data": ... } のみが返る。エラー時は共通のエラー封筒が返る。
個人情報を返さない設計
トークンさえ知っていれば誰でもアクセスできるため、レスポンスには内部 ID・メールアドレス・面接官の詳細を含めない。返るのはプロジェクト名・候補者名・枠情報に限られる。
共通のパスパラメータ
| フィールド | ルール |
|---|---|
token |
必須。1 文字以上。scheduling_tokens.token の値 |
スケジュール情報取得
トークンの有効性、案件情報、確定状況を返す。画面の初期表示で呼ぶ。
URI
レスポンス(200 OK)
{
"data": {
"project": {
"name": "2026年春季ユーザーインタビュー",
"interview_duration_minutes": 60
},
"candidate": {
"name": "山田太郎"
},
"is_expired": false,
"is_already_scheduled": false,
"reservation": null
}
}
日程が確定済みの場合、reservation に公開用の予約情報が入る。
{
"data": {
"project": { "name": "2026年春季ユーザーインタビュー", "interview_duration_minutes": 60 },
"candidate": { "name": "山田太郎" },
"is_expired": false,
"is_already_scheduled": true,
"reservation": {
"scheduledAt": "2026-02-03T01:00:00.000Z",
"durationMinutes": 60,
"interviewType": "online_meet",
"meetingUrl": "https://meet.google.com/xxx-yyyy-zzz",
"status": "confirmed"
}
}
}
| フィールド | 型 | 説明 |
|---|---|---|
project.name |
string | プロジェクト名 |
project.interview_duration_minutes |
integer | 面接時間(分) |
candidate.name |
string | 候補者の氏名 |
is_expired |
boolean | トークンが期限切れか |
is_already_scheduled |
boolean | 既に日程が確定しているか |
reservation |
object | null | 確定済みの予約情報(内部 ID・面接官情報を含まない) |
例外処理
| 説明 | ステータスコード | ステータス名 |
|---|---|---|
トークンが無効または期限切れ(SCHEDULING_TOKEN_ERROR) |
400 | Bad Request |
| トークンまたは候補者が存在しない | 404 | Not Found |
予約可能枠取得
面接官の空き状況から、候補者が選べる枠を返す。
URI
レスポンス(200 OK)
{
"data": [
{
"slotId": "slot_20260203_1000",
"interviewerId": 5,
"interviewerName": "末成",
"startAt": "2026-02-03T01:00:00Z",
"endAt": "2026-02-03T02:00:00Z"
}
]
}
| フィールド | 型 | 説明 |
|---|---|---|
slotId |
string | 枠の識別子。送信時にそのまま返す |
interviewerId |
integer | 面接官のユーザー ID |
interviewerName |
string | 面接官の表示名 |
startAt / endAt |
string(date-time) | 枠の開始/終了 |
例外処理
| 説明 | ステータスコード | ステータス名 |
|---|---|---|
| トークンが無効または期限切れ | 400 | Bad Request |
| トークンが存在しない | 404 | Not Found |
| Google Calendar エラー | 502 | Bad Gateway |
希望日程送信
候補者が選んだ枠を送信する。
URI
リクエストボディ
{
"slots": [
{
"slotId": "slot_20260203_1000",
"interviewerId": 5,
"startAt": "2026-02-03T01:00:00Z",
"endAt": "2026-02-03T02:00:00Z"
},
{
"slotId": "slot_20260204_1400",
"interviewerId": 5,
"startAt": "2026-02-04T05:00:00Z",
"endAt": "2026-02-04T06:00:00Z"
}
]
}
バリデーションルール
| フィールド | ルール |
|---|---|
slots |
必須。1〜20 件の配列 |
slots[].slotId |
必須。1 文字以上 |
slots[].interviewerId |
必須。正の整数 |
slots[].startAt |
必須。ISO 8601(date-time) |
slots[].endAt |
必須。ISO 8601(date-time) |
レスポンス(200 OK)
例外処理
| 説明 | ステータスコード | ステータス名 |
|---|---|---|
| バリデーションエラー(0 件、21 件以上など) | 400 | Bad Request |
トークンが無効または期限切れ(SCHEDULING_TOKEN_ERROR) |
400 | Bad Request |
| 候補者が存在しない | 404 | Not Found |
既に確定予約がある(SLOT_NOT_AVAILABLE) |
409 | Conflict |
処理内容
- トークンを検証し、期限切れなら
SchedulingTokenError - 既に
confirmedの予約があればSlotNotAvailableError - 既存の希望日程をすべて削除してから、新しい枠を登録する(追記ではない)
- 候補者ステータスが
contacted/waiting_response/schedulingのいずれかならschedulingに更新する - 候補者へ受付完了メールを送る(失敗してもリクエスト全体は成功扱い)
sequenceDiagram
participant C as 候補者
participant API as Backend API
participant DB as PostgreSQL
participant M as Gmail
C->>API: POST /scheduling/{token}/submit-availability
API->>DB: scheduling_tokens を検証
API->>DB: 確定予約の有無を確認
API->>DB: candidate_available_dates を全削除
API->>DB: 新しい希望日程を登録
API->>DB: status を scheduling に更新
API->>M: 受付完了メールを送信
Note over API,M: 失敗してもログのみ(リクエストは成功)
API-->>C: 200 { data: { message, selectedCount } }
PROTOTYPE_MODE
PROTOTYPE_MODE=true の間は 5. のメール送信が行われない。
この後の流れ
送信された希望日程は管理画面の候補者詳細に availableDates として表示される。管理者が 候補者 API の POST /{candidateId}/confirm-schedule で 1 件を選ぶと予約が確定する。