EHS Pulse API Reference

Every REST endpoint exposed by the EHS Pulse backend, generated directly from the route definitions in backend/src/routes/. All endpoints are served under the API's base URL (e.g. http://localhost:9000 in the default Docker setup) and, unless marked Public, require an Authorization: Bearer <token> header obtained from POST /api/auth/login. Tenant scoping is automatic — the token determines which tenant's data a request can see; a Super Admin token may target another tenant by sending an X-Tenant-Id header (surfaced in-app via the tenant switcher in the topbar).

15 resource groups 95 endpoints JSON request/response bodies unless noted

Authentication /api/auth

Login, MFA enrollment/verification, session profile, and password management.

POST /api/auth/login Public
Authenticate with email + password. Returns a session token, or {mfaRequired:true, pendingToken} if the account has two-factor enabled.
Request{ email, password, tenantCode? }
Response{ token, user } or { mfaRequired, pendingToken }
POST /api/auth/mfa/login-verify Public (pendingToken)
Second step of login when MFA is enabled — verifies a 6-digit TOTP code against the challenge token from /login.
Request{ pendingToken, code }
Response{ token, user }
POST /api/auth/mfa/setup Bearer token
Starts MFA enrollment for the current user; generates a secret and returns a scannable QR code.
Request—
Response{ secret, qrDataUrl }
POST /api/auth/mfa/verify Bearer token
Confirms MFA enrollment by verifying a code against the secret from /mfa/setup, enabling two-factor on the account.
Request{ code }
Response{ message }
POST /api/auth/mfa/disable Bearer token
Disables MFA on the current account (requires the current password).
Request{ password }
Response{ message }
GET /api/auth/me Bearer token
Returns the current authenticated user's profile, tenant, and MFA status.
Request—
ResponseUser + tenant profile object
POST /api/auth/change-password Bearer token
Changes the current user's password, enforcing the tenant's configured password policy (min length, number/uppercase requirements).
Request{ currentPassword, newPassword }
Response{ message }
POST /api/auth/forgot-password Public
Requests a password reset. In this reference build, returns a dev reset token directly instead of emailing it.
Request{ email }
Response{ message, devResetToken? }

Tenants /api/tenants

Multi-tenant administration — every endpoint here requires the Super Admin role.

GET /api/tenants/ Super Admin
Lists every tenant with a per-tenant user count.
Request—
ResponseTenant[]
POST /api/tenants/ Super Admin
Creates a new tenant.
Request{ name, code, subscriptionPlan?, storageAllocationMb?, themeColor?, language?, timezone? }
Response{ id }
PUT /api/tenants/:id Super Admin
Updates a tenant's branding, plan, storage allocation, and password policy.
RequestAny subset of tenant fields
Response{ message }
POST /api/tenants/:id/activate Super Admin
Reactivates a suspended tenant.
Request—
Response{ message }
POST /api/tenants/:id/deactivate Super Admin
Suspends a tenant (blocks login for all its users).
Request—
Response{ message }

Users /api/users

Tenant user management. All endpoints require the manage_users permission and are scoped to the caller's tenant (except Super Admin, who may target other tenants via X-Tenant-Id) — except /lookup, deliberately open to any authenticated tenant user.

GET /api/users/lookup Bearer token
Lightweight user list ({id, name, email, department} for active users) — backs user_lookup fields ('Inspector', 'Assigned To') so anyone filling a form can search real users, without needing the admin-only manage_users permission GET / below requires.
Request—
Response{id, name, email, department}[]
GET /api/users/site-options Bearer token
Backs the Assigned Site dropdown on the user edit form — the actual value/label options from whichever master data set(s) are currently configured as a site-restriction gate (module_fields.site_restricted), so it always reflects real configuration rather than a guessed set.
Request—
Response{value, label}[]
GET /api/users/ manage_users
Lists every user in the tenant along with their assigned roles.
Request—
ResponseUser[]
POST /api/users/ manage_users
Creates a user with a random temporary password; sends an activation email if SMTP is configured and active.
Request{ name, email, employeeId?, mobile?, department?, designation?, reportingManagerId?, roleIds? }
Response{ id, tempPassword, message }
PUT /api/users/:id manage_users
Updates a user's profile fields and/or role assignments.
RequestAny subset of user fields, roleIds?
Response{ message }
POST /api/users/:id/deactivate manage_users
Deactivates a user, blocking further login.
Request—
Response{ message }
POST /api/users/:id/reset-password manage_users
Resets a user's password to a new random value; emails it if SMTP is configured.
Request—
Response{ tempPassword, message }
POST /api/users/bulk manage_users
Creates many users at once from a simple array; reports per-row success/failure.
Request{ users: [{name,email,department?,designation?}] }
Response{ created, failed[] }

Roles & Permissions /api/roles

RBAC configuration — roles, module/global permissions, field-level access, and single-level role inheritance (editable after creation, not just set at creation).

GET /api/roles/permission-keys Bearer token
Returns the fixed list of permission key constants the UI can assign (view, create, edit, delete, approve, export, print, manage_users, ...).
Request—
Responsestring[]
GET /api/roles/ manage_roles
Lists roles for the tenant with their permission sets and inherited-role name.
Request—
ResponseRole[]
POST /api/roles/ manage_roles
Creates a role, optionally inheriting another role's allow permissions.
Request{ name, code, description?, inheritsRoleId? }
Response{ id }
PUT /api/roles/:id manage_roles
Updates a role's name, description, or inheritance — including after creation, from the role's "Edit" button.
Request{ name?, description?, inheritsRoleId? }
Response{ message }
DELETE /api/roles/:id manage_roles
Deletes a non-system role (system roles are protected).
Request—
Response{ message }
PUT /api/roles/:id/permissions manage_roles
Replaces a role's full module/global permission set. Deny always overrides allow.
Request{ permissions: [{permissionKey, moduleId?, effect}] }
Response{ message }
PUT /api/roles/:id/field-permissions manage_roles
Replaces a role's field-level access (view/edit/hide) for one module.
Request{ moduleId, fields: [{fieldKey, access}] }
Response{ message }

Modules /api/modules

The Dynamic Module Builder — define and version a module's field schema, including cross-module record_lookup fields, archive/reactivate, duplication, and a printable blank-form PDF export.

GET /api/modules/ Bearer token
Lists modules for the tenant with a live record count per module. Archived (status='inactive') modules are excluded by default — pass ?status=all to include them (used by the Module Builder's module picker, so an archived module can still be found and reactivated).
RequestQuery: status?
ResponseModule[]
GET /api/modules/:id Bearer token
Gets one module with its ordered field schema.
Request—
ResponseModule + fields[]
GET /api/modules/:id/child-modules Bearer token
Which other module(s) hold "child" records of this one — i.e. have a record_lookup field whose target is this module. Schema-level version of GET /records/:id/children below — used by the Guided Checklist flow to find where its per-item answers belong without hardcoding a module id.
Request—
Response{moduleId, moduleName, moduleCode, fieldKey, fieldLabel}[]
POST /api/modules/ manage_modules
Creates a new module with an initial field schema.
Request{ name, code, description?, icon?, category?, fields?: Field[] }
Response{ id }
PUT /api/modules/:id manage_modules
Updates module metadata (name/description/icon/category/status); bumps the version number. Also how a module is reactivated (status: 'active').
RequestAny subset of module fields
Response{ message }
PUT /api/modules/:id/fields manage_modules
Replaces the field schema. Existing record data is never touched by a schema change.
Request{ fields: Field[] }
Response{ message }
DELETE /api/modules/:id manage_modules
Archives a module (status → 'inactive'); soft delete. Existing records and any dashboards/reports built on it stay fully intact — this only hides it from the sidebar nav.
Request—
Response{ message }
GET /api/modules/:id/export Bearer token
Exports a module's structure as portable JSON — name/description/icon/category plus the full field schema. A master_lookup/record_lookup field's dependency is resolved to that master data set's/module's *code* rather than its internal database id, so the file means the same thing on a different tenant or a fresh install, not just as a same-tenant backup.
Request—
Response{ _exportFormat, name, code, description, icon, category, fields: Field[] }
POST /api/modules/import manage_modules
Creates a new module from the JSON structure the export endpoint above produces (or a hand-written equivalent). The requested code is auto-suffixed on collision rather than rejected outright. A master_lookup/record_lookup field's masterDataSetCode/targetModuleCode is resolved against this tenant's own master data sets/modules; if nothing matches, the field is still created without that link configured and reported back as a warning rather than failing the whole import.
Request{ name, code, description?, icon?, category?, fields: Field[] }
Response{ id, code, warnings: string[] }
GET /api/modules/:id/blank-form.pdf Bearer token
Generates a printable blank paper form — every field laid out with somewhere to write by hand, grouped by section, master_lookup options printed as checkboxes, signature fields get a boxed sign-and-date line.
Request—
ResponsePDF file

Records /api/records

Generic CRUD for data stored against any dynamic module, validated against that module's live field schema — including conditional-visibility-aware required-field enforcement.

GET /api/records/module/:moduleId view
Lists records for a module — paginated, with optional status filter and free-text search.
RequestQuery: page, pageSize, status?, search?
Response{ rows[], total, page, pageSize }
GET /api/records/:id view
Gets one record with its full workflow history.
Request—
ResponseRecord + history[]
GET /api/records/:id/children view
Reverse lookup — every other record, in any module, whose record_lookup field points at this one (e.g. every Audit Checklist Item belonging to a Safety Audit & Inspection header, or every Corrective Action raised against a checklist item). Generic, not hardcoded to one relationship; grouped by the field that references this record.
Request—
Response{moduleId, moduleName, moduleCode, fieldKey, fieldLabel, records: [{id, record_no, workflow_status, created_at}]}[]
POST /api/records/module/:moduleId/import create
Bulk-imports up to 500 rows at once (used by CSV import and the Guided Checklist flow), validating each row independently.
Request{ rows: object[] }
Response{ created, failed: [{row, reasons}] }
POST /api/records/module/:moduleId create
Creates one record; auto-generates its record number and initial workflow stage if the module has an active workflow. For any master_lookup field pointing at a hierarchical master-data set, if the submitted value is a child (e.g. an Area under a Department), also derives and stores its top-level ancestor under `${field_key}_parent` — so dashboards can group by either the specific value picked or the rolled-up ancestor.
Request{ data: object }
Response{ id, recordNo }
PUT /api/records/:id edit
Updates a record (partial merge with existing data), re-validated against the schema.
Request{ data: object }
Response{ message }
DELETE /api/records/:id delete
Permanently deletes a record.
Request—
Response{ message }

Workflows /api/workflows

No-code workflow builder and execution engine — stages, role-based transitions, field-value conditions, and the resulting record-level action history.

GET /api/workflows/my-tasks Bearer token
Powers the homepage ‘My Pending Tasks’ widget — aggregates across every module with an active workflow: records sitting in a stage whose outgoing transition is assigned to a role the current user holds, and records with a user_lookup field (e.g. Assigned To) pointing at the current user. Scoped to non-final stages only.
Request—
Response{moduleId, moduleName, moduleCode, recordId, recordNo, stage, reason: 'approval'|'assigned'|'both', createdAt}[]
GET /api/workflows/ Bearer token
Lists workflows for the tenant, optionally filtered to one module.
RequestQuery: moduleId?
ResponseWorkflow[]
GET /api/workflows/:id Bearer token
Gets one workflow with its full stage and transition graph.
Request—
ResponseWorkflow + stages[] + transitions[]
POST /api/workflows/ manage_modules
Creates a complete workflow — stages and transitions — in a single save.
Request{ moduleId, name, description?, slaResponseMinutes?, slaResolutionMinutes?, stages[], transitions[] }
Response{ id }
PUT /api/workflows/:id/status manage_modules
Changes a workflow's status (active / inactive / draft).
Request{ status }
Response{ message }
POST /api/workflows/records/:recordId/action edit
Executes a transition on a record — validates the action is legal from its current stage and that any field-value conditions on the transition are met, then applies it and sends any matching notification.
Request{ actionName, comments? }
Response{ message, newStage }
GET /api/workflows/records/:recordId/available-actions Bearer token
Returns the actions currently valid for a record, pre-filtered to ones whose conditions already pass — so the UI never shows a button that would fail.
Request—
Response{ action_name, to_stage }[]

Dashboards /api/dashboards

Dashboard builder — widgets computed live from module data, with count or sum/avg/min/max aggregation per group, drag-and-drop ordering, and optional auto-refresh.

GET /api/dashboards/ Bearer token
Lists dashboards visible to the caller (own, department, or organization-wide).
Request—
ResponseDashboard[]
POST /api/dashboards/ Bearer token
Creates a dashboard.
Request{ name, visibility?, refreshSeconds? }
Response{ id }
PUT /api/dashboards/:id Bearer token
Updates a dashboard's name, visibility, or auto-refresh interval.
Request{ name?, visibility?, refreshSeconds? }
Response{ message }
POST /api/dashboards/:id/widgets Bearer token
Adds a widget to a dashboard. widgetType includes 'funnel' (how many records have ever reached each of the module's workflow stages, via workflow_history — not just the current-moment distribution). config supports an optional filter ({field, operator, value}, operators: equals/not_equals/gt/lt/gte/lte/in) narrowing which records are counted or grouped, and an optional dateTrunc: 'month' on a date groupBy field for month-bucketed trends.
Request{ widgetType, title, moduleId?, config?, position? }
Response{ id }
DELETE /api/dashboards/widgets/:widgetId Bearer token
Removes a widget.
Request—
Response{ message }
PUT /api/dashboards/widgets/:widgetId Bearer token
Updates a single widget's position/size.
Request{ position }
Response{ message }
PUT /api/dashboards/:id/widgets/reorder Bearer token
Bulk-updates widget display order after a drag-and-drop reorder.
Request{ order: [{id, order}] }
Response{ message }
GET /api/dashboards/:id Bearer token
Gets a dashboard with every widget's data computed live at request time.
Request—
ResponseDashboard + widgets[] (each with .data)
DELETE /api/dashboards/:id Bearer token
Deletes a dashboard and its widgets.
Request—
Response{ message }

Reports /api/reports

Custom report builder — field selection, filters, grouping with sum/avg/min/max aggregation, CSV/PDF export, and scheduled email delivery (handled by the background scheduler).

GET /api/reports/ Bearer token
Lists report definitions for the tenant.
Request—
ResponseReport[]
POST /api/reports/ export
Creates a report definition — fields, filters, optional grouping/aggregation, and an optional daily/weekly/monthly email schedule.
Request{ name, moduleId, reportType?, fields[], filters?, groupBy?, aggregations?, schedule?, scheduleRecipients?, delivery? }
Response{ id }
POST /api/reports/:id/run export
Executes a report immediately and returns the resulting rows (grouped/aggregated if configured).
Request—
Response{ rows[], count }
GET /api/reports/:id/export.csv export
Downloads the report's current results as a CSV file.
Request—
ResponseCSV file
GET /api/reports/:id/export.pdf export
Downloads the report's current results as a simple tabular PDF.
Request—
ResponsePDF file
DELETE /api/reports/:id export
Deletes a report definition.
Request—
Response{ message }

Module Notifications /api/module-notifications

Configurable notification and reminder emails — a fully customizable sibling to the fixed six BRD notification triggers and the Schedule feature. Two trigger types: on_create fires once immediately with record-specific merge tokens; recurring fires on a schedule with an optional end date.

GET /api/module-notifications/ Bearer token
Lists notifications — scoped to one module with ?moduleId=, or every notification in the tenant otherwise.
RequestQuery: moduleId?
ResponseModuleNotification[]
POST /api/module-notifications/ manage_modules
Creates a notification. For triggerType=recurring, frequency is required; endDate is optional and, once past, the notification stops firing on its own without needing to be paused. Subject/body support {{record_no}} and {{field_key}} merge tokens, resolved for on_create only (a recurring reminder has no single record to merge against).
Request{ moduleId, name, triggerType: 'on_create'|'recurring', frequency?, endDate?, toEmails, ccEmails?, subject, body }
Response{ id }
POST /api/module-notifications/preview manage_modules
Live preview of a subject/body as currently drafted, before saving. For triggerType=on_create, merge tokens are resolved against the module's single most recently created record (a real one, not placeholder text) — user_lookup fields resolve to the person's name. A recurring reminder has no per-record context, so it returns the raw text unchanged, matching actual send behavior.
Request{ moduleId, triggerType, subject, body }
Response{ subject, body, usedRecord, note? }
PUT /api/module-notifications/:id manage_modules
Pauses or resumes a notification.
Request{ isActive }
Response{ message }
DELETE /api/module-notifications/:id manage_modules
Deletes a notification.
Request—
Response{ message }

Inspection Schedules /api/schedules

Recurring inspection reminders (Mitti's "Create schedule" template action) — can't auto-generate inspection data since answering a checklist needs a human, so this reminds someone instead: a daily/weekly/monthly email, sent by the same background scheduler that handles report delivery and SLA escalation.

GET /api/schedules/ Bearer token
Lists reminder schedules — scoped to one module with ?moduleId=, or every schedule in the tenant otherwise.
RequestQuery: moduleId?
ResponseSchedule[]
POST /api/schedules/ manage_modules
Creates a recurring reminder schedule for a module.
Request{ moduleId, name, frequency: 'daily'|'weekly'|'monthly', assignedUserId }
Response{ id }
PUT /api/schedules/:id manage_modules
Pauses or resumes a schedule.
Request{ isActive }
Response{ message }
DELETE /api/schedules/:id manage_modules
Deletes a reminder schedule.
Request—
Response{ message }

Email Configuration /api/email

Per-tenant SMTP configuration, the six notification-trigger templates, and a send log.

GET /api/email/config manage_email
Gets the tenant's SMTP configuration (the stored password is never returned).
Request—
ResponseEmailConfig | null
PUT /api/email/config manage_email
Saves the tenant's SMTP configuration.
Request{ smtpHost, smtpPort, encryption, senderEmail, senderName, smtpUsername?, smtpPassword?, isActive }
Response{ message }
POST /api/email/config/test manage_email
Sends a test email using the saved configuration.
Request{ to? }
Response{ message }
GET /api/email/templates manage_email
Lists the six notification templates (user_created, password_reset, workflow_submission, workflow_approval, workflow_rejection, escalation_alert).
Request—
ResponseTemplate[]
PUT /api/email/templates/:triggerKey manage_email
Updates one notification template's subject/body/active state.
Request{ subject, bodyHtml, isActive }
Response{ message }
GET /api/email/logs manage_email
Lists the most recent 200 email send attempts (sent/failed) for the tenant.
Request—
ResponseEmailLog[]

Audit Trail /api/audit

Read-only access to the full audit log — every mutating action across the platform, with user, timestamp, IP, and before/after values.

GET /api/audit/ view_audit
Queries the audit trail with pagination and optional filters.
RequestQuery: page, pageSize, action?, entityType?, userId?, from?, to?
Response{ rows[], total, page, pageSize }

Master Data /api/master-data

Shared named lookup lists — back Master Data Lookup module fields, and double as the source for the Guided Checklist flow's predefined checklists (a set coded checklist_<type>).

GET /api/master-data/ Bearer token
Lists master data sets for the tenant with a value count per set.
Request—
ResponseMasterDataSet[]
GET /api/master-data/:id Bearer token
Gets one set with its full ordered value list.
Request—
ResponseMasterDataSet + values[]
GET /api/master-data/:id/values Bearer token
Lightweight value list only — what data-entry dropdowns actually fetch.
Request—
Response{ value, label }[]
POST /api/master-data/ manage_modules
Creates a new master data set.
Request{ name, code, description? }
Response{ id }
PUT /api/master-data/:id manage_modules
Updates a set's name/description.
Request{ name?, description? }
Response{ message }
DELETE /api/master-data/:id manage_modules
Deletes a set (fields linked to it lose their option list).
Request—
Response{ message }
PUT /api/master-data/:id/values manage_modules
Replaces the full value list for a set in one save.
Request{ values: [{value, label}] }
Response{ message }
POST /api/master-data/:id/values/add manage_modules
Adds one value, optionally with a parent (for a two-level hierarchy, e.g. Department -> Area) and a head user.
Request{ value, label?, parentValueId?, headUserId?, displayOrder? }
Response{ id }
POST /api/master-data/:id/values/import manage_modules
Bulk imports values from Excel/CSV — the safe alternative to the destructive bulk-replace endpoint above for a set with hierarchy, since every row is an individual create-or-update rather than a delete-everything-then-reinsert. parent and headEmail are human-editable references (a value code, an email address), not raw ids — a parent referenced by a later row resolves correctly as long as it appears earlier in the same file. Limited to 500 rows.
Request{ rows: [{value, label?, parent?, headEmail?}] }
Response{ created, updated, failed: [{row, reason}] }
PUT /api/master-data/values/:valueId manage_modules
Updates one value's label/parent/head/order.
Request{ value?, label?, parentValueId?, headUserId?, displayOrder? }
Response{ message }
DELETE /api/master-data/values/:valueId manage_modules
Deletes one value. Any values nested under it (parent_value_id pointing at it) are deleted too.
Request—
Response{ message }

Files /api/files

File upload backing the File / Image / Signature field types, storing tenant-scoped attachments on a persistent Docker volume.

POST /api/files/upload Bearer token
Uploads one file (multipart/form-data, field name "file"). 15MB limit; blocks executable file types. Also how a drawn signature is saved — the canvas is captured as a PNG and uploaded through this same endpoint.
Requestmultipart/form-data: file
Response{ url, filename, size, mimeType }