Incidents
The incident endpoints cover the full lifecycle:
open → acknowledged → in_progress → resolved → verified. Auth levels per
endpoint are noted inline - see Authentication for
how sessions and the mobile bearer key work, and the field-value notes in
Status & assets for the shared incident fields
(status, severity, reporterName, slaBreached, source,
isDowntime).
GET /api/incidents
Section titled “GET /api/incidents”Auth: public read.
Query filters, all optional and combinable:
?status=&severity=&assetId=&unitId=. Merged duplicates are hidden unless
you pass includeMerged=true.
[ { "id": 17, "title": "Elevator A2 stuck on floor 7", "description": "…", "severity": "critical", "status": "in_progress", "isDowntime": true, "assetId": 2, "assetName": "Elevator A2", "unitId": null, "unitName": null, "reporterName": "Anonymous resident", "slaBreached": true, "source": "web", "createdAt": "2026-06-08T09:12:00.000Z", "resolvedAt": null }]Asset/unit names and the reporter display name are joined in; exactly one of
assetId/unitId is set (except untriaged source: "email" incidents,
which may have neither until staff assign one).
POST /api/incidents
Section titled “POST /api/incidents”Auth: session or bearer key (writes require auth).
Create an incident. Accepts JSON, or multipart form data when
attaching photos (photos[] file parts alongside the fields).
{ "assetId": 2, "title": "Elevator A2 stuck on floor 7", "description": "Car stopped between 7 and 8, alarm pressed.", "severity": "high", "isAnonymous": false}- Provide either
assetIdorunitId- exactly one. severity:low | medium | high | critical.isAnonymous: truehides the reporter’s name from non-staff everywhere (shown as"Anonymous resident"); staff still see the real name.- The incident starts
open; the reporter automatically follows it;sourceis set from how the request authenticated (web session →web, bearer →mobile). - Fires the
incident.createdwebhook event and writes an audit entry.
curl -H "Authorization: Bearer dev-mobile-key" \ -H "Content-Type: application/json" \ -d '{"assetId":2,"title":"Elevator making grinding noise","severity":"medium"}' \ http://localhost:3000/api/incidentsGET /api/incidents/:id
Section titled “GET /api/incidents/:id”Auth: public read.
The incident plus its updates (timeline) and photos:
{ "id": 17, "title": "Elevator A2 stuck on floor 7", "severity": "critical", "status": "resolved", "reporterName": "J. Park", "slaBreached": true, "source": "web", "createdAt": "2026-06-08T09:12:00.000Z", "resolvedAt": "2026-06-09T16:40:00.000Z", "updates": [ { "id": 88, "authorName": "M. Okafor", "body": "Vendor dispatched.", "newStatus": "in_progress", "createdAt": "…" } ], "photos": [ { "id": 31, "path": "incidents/17/a1b2.jpg", "createdAt": "…" } ]}newStatus on an update is set when that update changed the incident’s
status, null for plain comments. Photo paths resolve via
GET /api/photos/<path>.
Merged incidents: requesting a merged duplicate returns its
mergedInto/redirectTo target rather than a normal payload - follow it to
the surviving incident (the web page does this redirect automatically).
POST /api/incidents/:id/updates
Section titled “POST /api/incidents/:id/updates”Auth: session or bearer key. Status changes: staff only.
{ "body": "Vendor on site, parts ordered.", "newStatus": "in_progress" }Anyone authenticated can comment (body only). Including newStatus moves
the incident through its lifecycle and is restricted to roles
board/manager/admin. The first transition past open stamps
acknowledgedAt - the moment the SLA measures. Status changes notify
followers, fire incident.status_changed, and are audited.
POST /api/incidents/:id/verify
Section titled “POST /api/incidents/:id/verify”Auth: any signed-in user, or bearer key.
The resident verification step, valid on resolved incidents:
{ "fixed": true }"fixed": true→ status becomesverified(terminal)."fixed": false→ status returns toin_progress(reopened).
Both outcomes notify followers and write audit entries. This endpoint is deliberately open to all signed-in residents - see Following & verified fixes.
POST /api/incidents/:id/merge
Section titled “POST /api/incidents/:id/merge”Auth: staff only.
{ "targetId": 12 }Merges incident :id (the duplicate) into targetId: the dupe is closed
out with mergedIntoId, its reporter and followers are copied to the
target, the target’s timeline gains a “Merged INC-x into this incident”
entry, the incident.merged webhook event fires, and the merge is audited.
The dupe thereafter redirects to the target.
POST /api/push/register
Section titled “POST /api/push/register”Auth: session or bearer key.
Registers a device for Expo push notifications (upsert on token):
{ "token": "ExponentPushToken[xxxxxxxxxxxxxxxxxxxxxx]", "platform": "ios" }platform: ios | android. Followers with registered tokens receive push
on incident.created / status_changed / sla_breached / merged events
relevant to incidents they follow (delivery requires
EXPO_PUSH_ENABLED=true on the server).
GET /api/export/incidents.csv
Section titled “GET /api/export/incidents.csv”Auth: staff only.
CSV export of the incident list. Accepts the same filters as
GET /api/incidents (status, severity, assetId, unitId,
includeMerged):
curl -b jar.txt \ "http://localhost:3000/api/export/incidents.csv?assetId=2&status=resolved" \ -o elevator-a2-resolved.csv