コンテンツにスキップ

日程調整 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

GET /api/v1/scheduling/{token}

レスポンス(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

GET /api/v1/scheduling/{token}/slots

レスポンス(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

POST /api/v1/scheduling/{token}/submit-availability

リクエストボディ

{
  "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)

{
  "data": {
    "message": "希望日程を受け付けました",
    "selectedCount": 2
  }
}

例外処理

説明 ステータスコード ステータス名
バリデーションエラー(0 件、21 件以上など) 400 Bad Request
トークンが無効または期限切れ(SCHEDULING_TOKEN_ERROR) 400 Bad Request
候補者が存在しない 404 Not Found
既に確定予約がある(SLOT_NOT_AVAILABLE) 409 Conflict

処理内容

  1. トークンを検証し、期限切れなら SchedulingTokenError
  2. 既に confirmed の予約があれば SlotNotAvailableError
  3. 既存の希望日程をすべて削除してから、新しい枠を登録する(追記ではない)
  4. 候補者ステータスが contacted / waiting_response / scheduling のいずれかなら scheduling に更新する
  5. 候補者へ受付完了メールを送る(失敗してもリクエスト全体は成功扱い)
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 件を選ぶと予約が確定する。