{"repo": "aviraldeshratna11/interviehire", "files": {"api.md": {"462099871df8f817992d10ba9ea1d97d1cdc1118": {"t": 1785746392.4610255, "n": 253619, "h": "e12e52fdf0b1dbfe", "c": "# IntervieHire API Reference\n\n> This file is the **single source-of-truth API contract** for IntervieHire across all three services. It **MUST be kept in sync on every route change** — any time an API route is added, modified, refactored, or removed, this file is updated in the same unit of work. See `CLAUDE.md` (\"`api.md` is a living API contract\") for the enforcement rules.\n\n## Changelog\n\n> Append-only, newest first. A new entry is **prepended** here whenever a route is added, modified, refactored, or removed. Never rewrite history.\n\n- **2026-07-22** — **Optional expiry (application deadline) on the public apply link** (`backend/app/models/job.py`, `backend/main.py:init_db()`, `backend/app/schemas.py`, `backend/app/routers/jobs.py`, `backend/app/routers/public.py`; dashboard `apply-share-panel.ts`, `api.ts`). New NULLABLE column **`jobs.applications_close_at`** (`TIMESTAMP WITH TIME ZONE`, added via `init_db()` `ADD COLUMN IF NOT EXISTS`) — an optional deadline for the candidate-facing apply link; `NULL` = no expiry (the link stays open while the job is `is_job_listed && published`, unchanged). **Request:** **PATCH `/api/jobs/{job_id}/settings`** (`JobSettingsIn`) gains optional **`applications_close_at: datetime|null`** — send an ISO-8601 datetime to set the deadline, explicit `null` to clear it (reopen), or omit the key to leave it unchanged (`exclude_unset`). **Response:** **`JobDetailOut`** now serializes **`applications_close_at: datetime|null`** (affects GET `/api/jobs/{job_id}`, POST `/api/jobs`, POST `/api/jobs/{job_id}/duplicate`, PATCH `/api/jobs/{job_id}/settings`, PATCH `/api/jobs/{job_id}/parameters`). **Public apply behavior** (`backend/app/routers/public.py`, `_applications_closed()` helper): when `applications_close_at` is set and in the past, **GET `/api/public/apply/{job_id}`** and **GET `/api/public/careers/{subdomain}/apply/{job_id}`** now render an \"Applications closed\" HTML page (still 200) instead of the form, and the matching **POST**s return **410 Gone** `\"Applications for this role are closed.\"` (the `is_job_listed && published` 404 gate is unchanged and checked first). No routes added or removed. (Dashboard: the \"Apply link & share\" panel now builds a full absolute URL from the new `NEXT_PUBLIC_PUBLIC_BASE_URL` env — `${base}/api/public/apply/{jobId}`, default `https://app.interviehire.com` (the dashboard origin already proxies `/api/public/*` to the backend first-party via `next.config.js`) — generates the link on demand plus a QR rendered fully client-side (dynamic-imported `qrcode`, no third-party QR service) with a PNG \"Download QR\" button (on click, no eager external QR fetch), and offers a per-job deadline control wired to the settings PATCH.)\n- **2026-07-20** — **Richer custom-application-question types** (`backend/app/utils/application_questions.py`, `backend/app/routers/public.py`; recruiter editor `dashboard/src/dashboard/application-questions-editor.ts`). The question-definition `type` enum is **extended from `short_text|long_text|boolean|select` to `short_text|long_text|boolean|select|multi_select|number|date|file`** (`ALLOWED_TYPES`). Options are now carried for **both** `select` **and** `multi_select` (`OPTION_TYPES = (select, multi_select)`); `number`/`date`/`file` carry no options. This changes the accepted value space of `application_questions` on **PATCH `/api/jobs/{job_id}/parameters`** (`JobParametersIn.application_questions`) and **PUT/POST `/api/organisation`** (`OrganisationIn.application_questions`) — same field/shape, additional valid `type` values (`normalize_questions` still drops unknown types to `short_text`). **Public apply-form behavior** (`GET`/`POST` `/api/public/careers/{subdomain}/apply/{job_id}` and `/api/public/apply/{job_id}`): the GET form now renders `number` → ``, `date` → ``, `file` → `` (accept-list below), and `multi_select` → a checkbox group whose boxes all share the multipart field name `aq__`. On **POST**, `collect_answers` reads `multi_select` via `form.getlist(\"aq__\")` and stores the chosen options as a comma-joined string; `number`/`date` are stored as trimmed text; a `file`-type answer stores the uploaded file's original name, and the bytes are streamed to `uploads/attachments/` (shared `safe_upload_path` traversal defense, **≤ 10 MB** `MAX_ATTACHMENT_BYTES`, extension allowlist `ALLOWED_ATTACHMENT_EXTS` = `.pdf/.doc/.docx/.txt/.rtf/.png/.jpg/.jpeg/.gif/.csv/.xls/.xlsx/.ppt/.pptx`), and the stored answer object gains an additional **`url`** field pointing at the saved path. Required-question enforcement is unchanged (blank required question → **400** `\"Please answer: