Interviewers API
Registering interviewers on a project, plus availability calculation and tentative holds via Google Calendar. See API Conventions for shared rules.
Base path: /api/v1/projects/{projectId}/interviewers
| Method | Path | Summary | Role |
|---|---|---|---|
| GET | / |
List interviewers | viewer |
| POST | / |
Add an interviewer | member |
| DELETE | /{userId} |
Remove an interviewer | member |
| GET | /availability |
Get interviewer availability | viewer |
| POST | /tentative-slots |
Create tentative calendar events | member |
| DELETE | /tentative-slots |
Delete tentative calendar events | member |
All endpoints require Authorization: Bearer <access_token>; writes require member or above.
List Interviewers
URI
Response (200 OK)
data is an array of Interviewer (not paginated).
{
"success": true,
"status": "success",
"statusCode": 200,
"data": [
{
"id": 3,
"projectId": 1,
"userId": 5,
"user": {
"id": 5,
"email": "interviewer@example.com",
"name": "Suenari",
"picture": null,
"role": "member",
"createdAt": "2026-01-10T09:00:00Z"
},
"createdAt": "2026-01-15T09:00:00Z"
}
],
"path": "/api/v1/projects/1/interviewers",
"method": "GET"
}
| Field | Type | Description |
|---|---|---|
id |
integer | project_interviewers.id |
projectId |
integer | Project ID |
userId |
integer | User ID |
user |
object | User details |
createdAt |
string(date-time) | When they were added |
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 |
Add Interviewer
URI
Request Body
Validation Rules
| Field | Rules |
|---|---|
userId |
Required positive integer referencing an existing user |
Obtain the user ID from the Users API listing.
Response (201 Created)
data holds the created Interviewer.
Errors
| Description | Status code | Status name |
|---|---|---|
| Validation error | 400 | Bad Request |
| Token missing or invalid | 401 | Unauthorized |
| Insufficient role | 403 | Forbidden |
| Project or user does not exist | 404 | Not Found |
| Already registered | 409 | Conflict |
uk_project_user (project_id + user_id) prevents duplicate registration.
Remove Interviewer
URI
Path Parameters
| Field | Rules |
|---|---|
projectId |
Required positive integer |
userId |
Required positive integer (users.id, not project_interviewers.id) |
Response (204 No Content)
No body.
Errors
| Description | Status code | Status name |
|---|---|---|
| Token missing or invalid | 401 | Unauthorized |
| Insufficient role | 403 | Forbidden |
| Interviewer does not exist | 404 | Not Found |
Get Interviewer Availability
Reads the Google Calendars of the interviewers registered on the project and computes bookable slots. Called by "Calculate slots" in STEP1.
URI
Query Parameters
| Field | Type | Required | Rules | Description |
|---|---|---|---|---|
startDate |
string | โ | YYYY-MM-DD |
Start of the window |
endDate |
string | โ | YYYY-MM-DD |
End of the window |
durationMinutes |
integer | - | positive integer | Interview length. Falls back to projects.interview_duration_minutes |
Example Request
GET /api/v1/projects/1/interviewers/availability?startDate=2026-02-01&endDate=2026-02-14&durationMinutes=90
Why durationMinutes exists
The interview length is only written to the project when STEP1 is saved. Pressing "Calculate slots" right after changing it on screen would otherwise compute with the previous value.
Slots span weekdays 9:00โ18:00 JST and start on multiples of the interview length (90 minutes gives 9:00-10:30 / 10:30-12:00 / โฆ). They never overlap, and none is returned that would run past business hours.
Response (200 OK)
data is an array of AvailableSlot.
{
"success": true,
"status": "success",
"statusCode": 200,
"data": [
{
"slotId": "slot_20260203_1000",
"interviewerId": 5,
"interviewerName": "Suenari",
"startAt": "2026-02-03T01:00:00Z",
"endAt": "2026-02-03T02:00:00Z"
}
],
"path": "/api/v1/projects/1/interviewers/availability",
"method": "GET"
}
| Field | Type | Description |
|---|---|---|
slotId |
string | Slot identifier, passed through when submitting preferred slots |
interviewerId |
integer | Interviewer's user ID |
interviewerName |
string | Interviewer's display name |
startAt / endAt |
string(date-time) | Slot start / end |
Slot length follows the project's interview_duration_minutes.
Errors
| Description | Status code | Status name |
|---|---|---|
| Malformed date | 400 | Bad Request |
| Token missing or invalid | 401 | Unauthorized |
| Project does not exist | 404 | Not Found |
Google Calendar error (GOOGLE_CALENDAR_ERROR) |
502 | Bad Gateway |
Create Tentative Calendar Events
Bulk-registers the slots settled in STEP1 as provisional events on the interviewers' Google Calendars. Existing tentative events are deleted and recreated.
URI
Request Body
{
"slots": [
{
"interviewerId": 5,
"startTime": "2026-02-03T10:00:00+09:00",
"endTime": "2026-02-03T11:00:00+09:00"
},
{
"interviewerId": 5,
"startTime": "2026-02-03T11:00:00+09:00",
"endTime": "2026-02-03T12:00:00+09:00"
}
]
}
Validation Rules
| Field | Rules |
|---|---|
slots |
Required array |
slots[].interviewerId |
Required integer |
slots[].startTime |
Required ISO 8601 string |
slots[].endTime |
Required ISO 8601 string |
Response (200 OK)
{
"success": true,
"status": "success",
"statusCode": 200,
"data": { "created": 20, "deleted": 0 },
"path": "/api/v1/projects/1/interviewers/tentative-slots",
"method": "POST"
}
| Field | Type | Description |
|---|---|---|
created |
integer | Tentative events created |
deleted |
integer | Pre-existing tentative events removed first |
The created event details are stored in projects.arrange_settings.step1.tentativeEventIds.
Events are created on the project calendar (projects.google_calendar_id) with the assigned interviewer as an attendee. Because many are created at once, no Google invitation is sent. If the project has no calendar yet, one is created at this point.
interviewerEmail in tentativeEventIds
Events used to be written to the interviewer's own calendar, so this field doubled as the calendar ID. It now only records which interviewer the slot is for and must not be used as a Calendar API calendar ID.
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 |
| Google Calendar error | 502 | Bad Gateway |
PROTOTYPE_MODE
While PROTOTYPE_MODE=true, nothing is written to the calendar.
Delete Tentative Calendar Events
Removes every tentative event for the project.
URI
Request
No body; only the projectId path parameter.
Response (200 OK)
{
"success": true,
"status": "success",
"statusCode": 200,
"data": { "deleted": 20 },
"path": "/api/v1/projects/1/interviewers/tentative-slots",
"method": "DELETE"
}
Note that this returns 200 with a deletion count, not 204.
Errors
| Description | Status code | Status name |
|---|---|---|
| Token missing or invalid | 401 | Unauthorized |
| Insufficient role | 403 | Forbidden |
| Project does not exist | 404 | Not Found |
| Google Calendar error | 502 | Bad Gateway |