Skip to content

Scheduling API (Public)

Public endpoints for candidates. They require no authentication โ€” access is granted by the issued scheduling token alone. See API Conventions for shared rules.

Base path: /api/v1/scheduling

Method Path Summary Auth
GET /{token} Get scheduling info Not required
GET /{token}/slots Get bookable slots Not required
POST /{token}/submit-availability Submit preferred slots Not required

The matching screen is the frontend's /scheduling/{token} (src/app/scheduling/[token]/).

The response shape differs from every other router

This router alone does not return the standard envelope (success / statusCode / timestamp / path / method). It returns only { "data": ... }. Errors still use the shared error envelope.

Designed to expose no personal data

Anyone with the token can reach these endpoints, so responses omit internal IDs, email addresses, and interviewer details. Only the project name, candidate name and slot information are returned.

Shared Path Parameter

Field Rules
token Required, at least 1 character; the value of scheduling_tokens.token

Get Scheduling Info

Returns token validity, request details and confirmation state. Called when the screen loads.

URI

GET /api/v1/scheduling/{token}

Response (200 OK)

{
  "data": {
    "project": {
      "name": "Spring 2026 User Interviews",
      "interview_duration_minutes": 60
    },
    "candidate": {
      "name": "Taro Yamada"
    },
    "is_expired": false,
    "is_already_scheduled": false,
    "reservation": null
  }
}

When a schedule is already confirmed, reservation carries the public reservation details.

{
  "data": {
    "project": { "name": "Spring 2026 User Interviews", "interview_duration_minutes": 60 },
    "candidate": { "name": "Taro Yamada" },
    "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"
    }
  }
}
Field Type Description
project.name string Project name
project.interview_duration_minutes integer Interview duration in minutes
candidate.name string Candidate's name
is_expired boolean Whether the token has expired
is_already_scheduled boolean Whether a schedule is already confirmed
reservation object | null Confirmed reservation (no internal IDs or interviewer details)

Errors

Description Status code Status name
Token invalid or expired (SCHEDULING_TOKEN_ERROR) 400 Bad Request
Token or candidate does not exist 404 Not Found

Get Bookable Slots

Returns the slots the candidate can pick, derived from interviewer availability.

URI

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

Response (200 OK)

{
  "data": [
    {
      "slotId": "slot_20260203_1000",
      "interviewerId": 5,
      "interviewerName": "Suenari",
      "startAt": "2026-02-03T01:00:00Z",
      "endAt": "2026-02-03T02:00:00Z"
    }
  ]
}
Field Type Description
slotId string Slot identifier, echoed back on submission
interviewerId integer Interviewer's user ID
interviewerName string Interviewer's display name
startAt / endAt string(date-time) Slot start / end

Errors

Description Status code Status name
Token invalid or expired 400 Bad Request
Token does not exist 404 Not Found
Google Calendar error 502 Bad Gateway

Submit Preferred Slots

Submits the slots the candidate selected.

URI

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

Request Body

{
  "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"
    }
  ]
}

Validation Rules

Field Rules
slots Required array of 1 to 20 items
slots[].slotId Required, at least 1 character
slots[].interviewerId Required positive integer
slots[].startAt Required ISO 8601 date-time
slots[].endAt Required ISO 8601 date-time

Response (200 OK)

{
  "data": {
    "message": "ๅธŒๆœ›ๆ—ฅ็จ‹ใ‚’ๅ—ใ‘ไป˜ใ‘ใพใ—ใŸ",
    "selectedCount": 2
  }
}

Errors

Description Status code Status name
Validation error (zero slots, more than 20, โ€ฆ) 400 Bad Request
Token invalid or expired (SCHEDULING_TOKEN_ERROR) 400 Bad Request
Candidate does not exist 404 Not Found
A confirmed reservation already exists (SLOT_NOT_AVAILABLE) 409 Conflict

What Happens

  1. Validate the token; raise SchedulingTokenError if expired
  2. Raise SlotNotAvailableError if a confirmed reservation already exists
  3. Delete every existing preferred slot, then insert the new ones (not appended)
  4. If the candidate's status is contacted / waiting_response / scheduling, move it to scheduling
  5. Send the acknowledgement email (a failure here does not fail the request)
sequenceDiagram
    participant C as Candidate
    participant API as Backend API
    participant DB as PostgreSQL
    participant M as Gmail

    C->>API: POST /scheduling/{token}/submit-availability
    API->>DB: Validate scheduling_tokens
    API->>DB: Check for a confirmed reservation
    API->>DB: Delete all candidate_available_dates
    API->>DB: Insert the new preferred slots
    API->>DB: Update status to scheduling
    API->>M: Send acknowledgement email
    Note over API,M: Failures are logged only (the request still succeeds)
    API-->>C: 200 { data: { message, selectedCount } }

PROTOTYPE_MODE

While PROTOTYPE_MODE=true, step 5 does not send anything.

What Comes Next

Submitted slots appear as availableDates on the candidate detail screen in the console. An admin picks one via POST /{candidateId}/confirm-schedule in the Candidates API to confirm the reservation.