API Conventions
Conventions shared by every endpoint of the interview arrangement REST API. Per-endpoint specifications live on the pages in the left menu.
Basics
| Item | Value |
|---|---|
| Base URL (local) | http://localhost:8080 |
| Base URL (dev) | https://interview-arrange-api.heineken.dev.4digit.ai |
| API prefix | /api/v1 |
| OpenAPI definition | GET /openapi.json (generated from route definitions) |
| Swagger UI | GET /docs |
| Health check | GET /health / GET /api/v1/ping |
| OpenAPI version | 3.0.3 |
Methods
The API is REST-based.
| Method | Purpose |
|---|---|
| GET | Retrieval |
| POST | Creation and execution (imports, sends, triggering syncs) |
| PATCH | Partial update |
| PUT | Full replacement (survey import settings, bulk candidate selection) |
| DELETE | Deletion |
Naming
To keep query parameters and JSON nodes consistent and readable, request URIs and JSON nodes use snake_case.
Responses are camelCase
Public request-body fields are snake_case, but the data section of responses is camelCase (interviewDurationMinutes). The conversion happens explicitly inside the route handler. Note that a few response fields (sent_count, failed_count, file_id, โฆ) remain snake_case.
Requests and Responses
Headers
Request headers
| Header | Required | Value |
|---|---|---|
Authorization |
On all but public endpoints | Bearer <access_token> |
Content-Type |
When a body is present | application/json (only the TSV upload uses multipart/form-data) |
X-Request-ID |
Optional | If supplied, the same ID appears in the response and logs |
Response headers
| Header | Value |
|---|---|
Content-Type |
application/json |
X-Request-ID |
Request tracing ID |
CORS
Only origins listed in ALLOWED_ORIGINS are allowed. Permitted methods are GET / POST / PUT / PATCH / DELETE / OPTIONS, with credentials: true and a 24-hour preflight cache.
Response Envelope
Every response uses the same envelope. The actual payload is always inside data.
Success (200 / 201)
{
"success": true,
"status": "success",
"statusCode": 200,
"timestamp": "2026-01-15T10:00:00.000Z",
"requestId": "1a2b3c4d",
"data": { "id": 1, "name": "Engineering Interview 2026" },
"path": "/api/v1/projects/1",
"method": "GET"
}
Paginated
data becomes an array and pagination is added.
{
"success": true,
"status": "success",
"statusCode": 200,
"timestamp": "2026-01-15T10:00:00.000Z",
"data": [ { "id": 1 }, { "id": 2 } ],
"pagination": {
"page": 1,
"limit": 20,
"totalCount": 100,
"totalPages": 5,
"hasNext": true,
"hasPrevious": false
},
"path": "/api/v1/projects",
"method": "GET"
}
Error
{
"success": false,
"status": "fail",
"statusCode": 404,
"timestamp": "2026-01-15T10:00:00.000Z",
"requestId": "1a2b3c4d",
"data": {
"code": "NOT_FOUND",
"error": "Not Found",
"message": "Project not found"
},
"path": "/api/v1/projects/999",
"method": "GET"
}
204 No Content
DELETE endpoints return 204 with no body; the envelope does not apply.
Unwrapping on the frontend
Orval's custom-fetch returns { data, status, headers }, so screens traverse response.data.data. Use the apiData() helper in lib/utils.ts.
Pagination
Shared query parameters for list endpoints.
| Parameter | Type | Default | Constraint | Description |
|---|---|---|---|---|
page |
integer | 1 |
Positive integer | Page number |
limit |
integer | 20 |
1โ100 | Items per page |
sort |
string | - | - | Format createdAt:desc |
Survey endpoints use offset/limit
Lists under /api/v1/surveys/* use offset / limit instead of page, and their data holds a custom { items, total, limit, offset } shape.
Authentication
Authentication uses a JWT issued by the backend based on user information obtained through Google OAuth 2.0. A valid Bearer token is required in the Authorization header.
sequenceDiagram
participant FE as Frontend
participant API as Backend API
participant G as Google OAuth
participant DB as PostgreSQL
FE->>API: GET /api/v1/auth/google
API-->>FE: 302 to the Google consent screen
FE->>G: Log in and consent
G->>API: GET /api/v1/auth/google/callback?code=...
API->>G: Exchange code for tokens
API->>DB: Find or create the user
API-->>FE: 302 to FRONTEND_URL (with token)
FE->>API: Subsequent calls with Authorization: Bearer <token>
API->>API: Verify the JWT (HS256)
| Item | Value |
|---|---|
| Signing algorithm | HS256 |
| Access token lifetime | JWT_EXPIRES_IN (default 1 hour) |
| Refresh token lifetime | JWT_REFRESH_EXPIRES_IN (default 7 days) |
| Payload | userId / googleId / email / name / role |
| Refresh | POST /api/v1/auth/refresh |
| Frontend storage | accessToken in localStorage |
Authorization (roles)
Roles are hierarchical: admin (3) > member (2) > viewer (1). Each router requires member or above for POST / PATCH / PUT / DELETE.
| Router | Auth | Role required for writes |
|---|---|---|
/api/v1/auth/* |
Partially (/logout, /me) |
- |
/api/v1/users |
Required | - |
/api/v1/projects/* |
Required | member |
/api/v1/projects/{id}/candidates/* |
Required | member |
/api/v1/projects/{id}/interviewers/* |
Required | member |
/api/v1/projects/{id}/email-templates/* |
Required | member |
/api/v1/projects/{id}/emails/* |
Required | member |
/api/v1/projects/{id}/reservations/* |
Required | member (PATCH / PUT / DELETE) |
/api/v1/email-templates |
Required | member |
/api/v1/surveys/* |
Required | None (read-only, but a viewer can read every survey) |
/api/v1/scheduling/* |
Not required (public) | - |
Error Handling
Status codes returned on failure:
| Description | Status code | Status name |
|---|---|---|
| Validation error | 400 | Bad Request |
| Token missing or invalid | 401 | Unauthorized |
| Insufficient role | 403 | Forbidden |
| Resource does not exist | 404 | Not Found |
| Duplicate or slot conflict | 409 | Conflict |
| Rate limit exceeded | 429 | Too Many Requests |
| Server internal error | 500 | Internal Server Error |
| External API error | 502 | Bad Gateway |
| DB / Databricks unreachable | 503 | Service Unavailable |
| External API timeout | 504 | Gateway Timeout |
Error Codes
Values placed in data.code. Generic errors are defined in src/lib/errors.ts, domain-specific ones in src/error.ts.
Generic
| Code | Status | Description |
|---|---|---|
BAD_REQUEST |
400 | Malformed request |
VALIDATION_ERROR |
400 | Zod validation failed; details carries per-field errors |
UNAUTHORIZED |
401 | Authentication required / token invalid |
FORBIDDEN |
403 | Insufficient permissions |
NOT_FOUND |
404 | Resource does not exist |
CONFLICT |
409 | Duplicate |
TOO_MANY_REQUESTS |
429 | Rate limit exceeded |
INTERNAL_ERROR |
500 | Internal error (details withheld from the client) |
Database
| Code | Status | Description |
|---|---|---|
DATABASE_ERROR |
500 | Generic DB error |
DATABASE_QUERY_ERROR |
500 | Query failure |
DATABASE_CONNECTION_ERROR |
503 | Connection failure |
DATABASE_CONSTRAINT_ERROR |
409 | Unique constraint violation, etc. |
External APIs
| Code | Status | Description |
|---|---|---|
EXTERNAL_API_ERROR |
502 | Generic external API error |
API_TIMEOUT |
504 | Timeout |
API_UNAVAILABLE |
503 | Temporarily unavailable |
GOOGLE_API_ERROR |
502 | Google API error |
GOOGLE_CALENDAR_ERROR |
502 | Google Calendar error |
GMAIL_API_ERROR |
502 | Gmail API error |
Databricks
| Code | Status | Description |
|---|---|---|
DATABRICKS_ERROR |
502 | Generic Databricks error |
DATABRICKS_QUERY_ERROR |
502 | Statement failed, was cancelled, or timed out |
DATABRICKS_AUTH_ERROR |
502 | OAuth M2M token request failed |
DATABRICKS_NOT_CONFIGURED |
503 | Databricks not configured (DATABRICKS_HOST or credentials missing) |
Domain-specific
| Code | Status | Description |
|---|---|---|
TOKEN_EXPIRED |
401 | JWT expired |
TOKEN_INVALID |
401 | JWT invalid |
SCHEDULING_TOKEN_ERROR |
400 | Scheduling token invalid or expired |
SLOT_NOT_AVAILABLE |
409 | The chosen slot is taken or already confirmed |
EMAIL_SEND_ERROR |
500 | Email delivery failed |
IMPORT_ERROR |
400 | Import error; carries row / column |
SCORING_ERROR |
400 | Invalid scoring rules |
INVALID_STATUS_TRANSITION |
400 | Disallowed status transition |
How errors are implemented
Handlers simply throw new NotFoundError('Project') and middlewares/error-handler.ts performs the conversion above. Errors with isOperational === false do not expose details to the client (stack traces only in development).
Endpoint Index
| Resource | Count | Page |
|---|---|---|
| Auth | 5 | Auth API |
| Users | 1 | Users API |
| Projects | 9 | Projects API |
| Candidates | 17 | Candidates API |
| Interviewers | 6 | Interviewers API |
| Emails | 10 | Emails API |
| Reservations | 3 | Reservations API |
| Scheduling (public) | 3 | Scheduling API |
| Surveys | 7 | Surveys API |