Skip to content

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.

/api/v1/projects/{projectId}/candidates/import/survey/preview
{
  "interview_duration_minutes": 60,
  "scheduling_range_start_days": 1
}

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