Admin & ops
The machine-facing operational endpoints: staff CRUD, email ingestion, the scheduled jobs, and the optional AI triage assist. Auth per endpoint varies - each section states it; details in Authentication.
Webhook CRUD - /api/admin/webhooks
Section titled “Webhook CRUD - /api/admin/webhooks”Auth: staff (session with role board/manager/admin).
GET /api/admin/webhooks listPOST /api/admin/webhooks createPATCH /api/admin/webhooks/:id update (any subset of fields)DELETE /api/admin/webhooks/:id deleteCreate/update body:
{ "url": "https://n8n.example/webhook/condopulse", "secret": "whsec_…", "events": ["incident.created", "incident.sla_breached", "asset.status_changed"], "active": true}Responses include delivery health: lastStatus (e.g. "200" or
"failed: timeout") and lastFiredAt. Event names, payload shape, and
signature verification are on the Webhooks page; the admin
UI equivalent is /admin → Webhooks.
Maintenance window CRUD - /api/admin/maintenance
Section titled “Maintenance window CRUD - /api/admin/maintenance”Auth: staff.
GET /api/admin/maintenance listPOST /api/admin/maintenance createPATCH /api/admin/maintenance/:id updateDELETE /api/admin/maintenance/:id deleteCreate/update body:
{ "assetId": 7, "title": "Monthly fire alarm test", "startsAt": "2026-07-01T10:00:00.000Z", "endsAt": "2026-07-01T11:00:00.000Z", "recurrence": "monthly", "notes": "First Tuesday, building-wide audible test"}recurrence: none | weekly | monthly. Windows are excluded from uptime
math and paint blue day slats - semantics in
Maintenance windows.
POST /api/inbound-email
Section titled “POST /api/inbound-email”Auth: Authorization: Bearer ${INBOUND_EMAIL_SECRET}.
Provider-agnostic email ingestion. Body:
{ "from": "resident@example.com", "subject": "Re: [INC-42] Elevator A2 stuck on floor 7", "text": "Plain-text body…", "html": "<p>Optional HTML body</p>", "attachments": [ { "filename": "photo.jpg", "contentBase64": "…", "contentType": "image/jpeg" } ]}Routing by subject:
- Subject contains
[INC-<id>]→ appended to that incident as a timeline update; attachments become incident photos. - Otherwise → creates a new untriaged incident:
source: "email", title = subject, description = text, severitymedium, no asset/unit yet (the one permitted exception to the one-of rule, until staff triage it in/admin→ Triage).
Processing is audited as the system actor. Wiring examples (Mailgun, n8n IMAP) in Triage queue & email-in.
Cron endpoints
Section titled “Cron endpoints”Auth: Authorization: Bearer ${CRON_SECRET} for both. Each also has a
CLI twin that hits the database directly (no server needed).
POST /api/cron/sweep (CLI: npm run sweep)
Section titled “POST /api/cron/sweep (CLI: npm run sweep)”The SLA escalation sweep. Finds incidents still open past their
time-to-acknowledge target (critical 4 h / high 24 h / medium 72 h /
low 168 h, env-overridable via SLA_HOURS_*) and, per incident: bumps
severity one level (max critical, once ever per incident), emails
board+, notifies followers, fires incident.sla_breached webhooks, and
audits. Run hourly. Full policy:
SLA policy & escalation.
POST /api/cron/digest (CLI: npm run digest)
Section titled “POST /api/cron/digest (CLI: npm run digest)”The weekly board digest: per-tower and per-asset downtime hours for the
week, new/resolved/verified counts, oldest unacknowledged incidents, and
upcoming maintenance - emailed to board/manager/admin users (or written to
.data/outbox/*.eml.html without SMTP). Run weekly. Contents and schedule
recipes: Weekly digest.
curl -X POST -H "Authorization: Bearer ${CRON_SECRET}" \ https://your-condopulse.example/api/cron/sweepPOST /api/triage/suggest
Section titled “POST /api/triage/suggest”Auth: signed-in session or bearer key.
Availability: only when the server has ANTHROPIC_API_KEY set and
the ai_triage feature flag is enabled; otherwise the endpoint reports
disabled and the report form hides the suggestion panel entirely.
Request:
{ "title": "Elevator grinding", "description": "A2 makes a loud grinding noise approaching the lobby…" }Response - a triage suggestion for the report form:
{ "assetId": 2, "severity": "high", "isDuplicateOf": 17, "rationale": "Matches Elevator A2; an open incident describes the same noise."}All fields except severity are optional in practice (assetId and
isDuplicateOf appear only when there’s a confident match). The endpoint is
built to never break the form: failures and timeouts return an empty
suggestion rather than an error, and the UI debounces calls (after typing
pauses, minimum description length applies). Suggestions are advisory - the
reporter applies or ignores them.
Related, but documented elsewhere
Section titled “Related, but documented elsewhere”GET /api/export/incidents.csv(staff) - with the incident endpoints: Incidents.GET /r/[assetId]- public QR redirect to the pre-filled report form: QR stickers.- Outgoing webhook deliveries - payloads, events, signatures: Webhooks.
Audit guarantee
Section titled “Audit guarantee”Every write documented on this page - CRUD, email ingestion, sweep and
digest runs - records an audit-log entry (human actor for staff calls,
system actor for secret-authenticated machine calls). The log is append-only
and admin-visible at /admin → Audit; see
Exports & audit log.