# API reference JSON over HTTP under `/api`. Authentication uses a session cookie (`safetylog_session`, http-only) set by setup, sign-in and password change. Request bodies are validated against the schemas in [`shared/schemas.ts`](../shared/schemas.ts). Errors look like `{ "error": "message" }`; validation errors (400) also include `issues: [{ path, message }]`. Roles, from least to most privileged: `reporter` < `supervisor` < `safety_manager` < `admin`. A role can do everything the roles below it can. ## Setup and authentication | Method | Path | Who | Notes | | --- | --- | --- | --- | | GET | `/api/health` | anyone | `{ ok: true }` | | GET | `/api/setup` | anyone | `{ needsSetup }`: true until the first admin exists | | POST | `/api/setup` | anyone, once | `{ organizationName, name, email, password }`. Creates the organization and an admin, signs them in. 409 afterwards | | POST | `/api/auth/login` | anyone | `{ email, password }`. 401 on bad credentials, 429 after 10 attempts per email in 15 minutes | | POST | `/api/auth/logout` | anyone | Ends the session. 204 | | GET | `/api/auth/me` | signed in | `{ user, organization }` | | POST | `/api/auth/password` | signed in | `{ currentPassword, newPassword }`. Signs out all other sessions. 204 | Passwords need at least 10 characters. ## Users | Method | Path | Who | Notes | | --- | --- | --- | --- | | GET | `/api/users` | safety_manager | All users in the organization | | POST | `/api/users` | admin | `{ name, email, role, password }`. 409 if the email exists | | PATCH | `/api/users/:id` | admin | `{ name?, role?, active? }`. Deactivating signs the user out. The last active admin can't be demoted or deactivated | ## Sites and areas | Method | Path | Who | Notes | | --- | --- | --- | --- | | GET | `/api/sites` | signed in | Sites with their `areas` (flat list; build the tree from `parentId`). `?includeArchived=true` to include archived | | POST | `/api/sites` | safety_manager | `{ name, address?, timezone? }` | | PATCH | `/api/sites/:id` | safety_manager | `{ name?, address?, timezone?, archived? }` | | POST | `/api/sites/:id/areas` | safety_manager | `{ name, kind?, parentId? }`. `kind`: `building`, `area` (default), `line`, `other`. Parent must be in the same site | | PATCH | `/api/areas/:id` | safety_manager | `{ name?, kind?, archived? }` | ## Incidents Incidents are addressed by their number (`/api/incidents/42` is `INC-0042`). Numbers are sequential per organization with no gaps. | Method | Path | Who | Notes | | --- | --- | --- | --- | | GET | `/api/incidents` | signed in | Paginated `{ items, page, pageSize, total }`, newest first. Reporters see only their own reports | | POST | `/api/incidents` | signed in | Create a report (below). 201 | | GET | `/api/incidents/:number` | signed in | 404 if it doesn't exist or isn't visible to you | | PATCH | `/api/incidents/:number` | supervisor | Any create field plus `status`. Closing sets `closedAt`; reopening clears it. Moving to another site clears the area unless one is given | **List query parameters:** `page` (default 1), `pageSize` (1–100, default 25), `q` (searches title, description and location; `INC-0042` or `42` matches the number), `type`, `severity`, `status`, `siteId`. **Create body:** ```json { "title": "Forklift reversed into pallet stack", "type": "near_miss", "severity": "high", "occurredAt": "2026-10-06T08:15:00-05:00", "siteId": "…", "areaId": "…", "locationDetail": "Loading bay C", "description": "…", "immediateAction": "Area cordoned off", "anonymous": false } ``` - `type`: `near_miss`, `hazard`, `injury`, `illness`, `property_damage`, `environmental` - `severity`: `low`, `medium`, `high`, `critical` - `status`: `reported`, `triage`, `investigation`, `actions`, `verification`, `closed` - `occurredAt` must include a time-zone offset and can't be in the future. - Anonymous reports don't record the reporter at all, so they won't appear in the reporter's own list. Every create and update is recorded in the audit log (`audit_log` table) with who made it and what changed.