Skip to content

Authentication

CondoPulse’s API is a set of JSON route handlers served by the web app (http://localhost:3000 in development). There are four credentials, each scoped to a job:

CredentialSent asUsed byScope
Session cookieCookie (set by sign-in)The web app, humansActs as the signed-in user, with their role
Mobile sessionAuthorization: Bearer <session-token>The mobile appActs as the signed-in user within their organization
Cron secretAuthorization: Bearer ${CRON_SECRET}SchedulersOnly POST /api/cron/sweep and POST /api/cron/digest
Inbound email secretAuthorization: Bearer ${INBOUND_EMAIL_SECRET}Your email provider’s webhookOnly POST /api/inbound-email

Public reads — building status, uptime, and tower names remain available at GET /api/status, GET /api/assets/:id, and GET /api/towers. Public asset responses contain no incident history.

Member reads — incident lists/details, unit lists, contacts, and incident photos require a session belonging to the property’s organization. Missing credentials return 401; foreign property lists return 403, and foreign incident/photo identifiers return 404. Invoice attachments additionally require a staff role. Send the bearer session on mobile image requests too.

Writes require either a session cookie or the mobile bearer session. Staff-only writes (status changes, merges, admin CRUD, exports) additionally require the authenticated identity to have role board, manager, or admin - the docs and reference pages call this staff.

Sign in at /login (email/password, or Google SSO when configured) and the browser carries a session cookie on every request. Server-rendered pages and fetch calls from the web app authenticate this way automatically.

For quick API poking with a session, sign in via the auth endpoint and reuse the cookie jar:

Terminal window
curl -c jar.txt -X POST http://localhost:3000/api/auth/sign-in/email \
-H "Content-Type: application/json" \
-d '{"email":"manager@demo.test","password":"demo1234"}'
curl -b jar.txt "http://localhost:3000/api/export/incidents.csv?status=resolved"

Your role (resident / board / manager / admin) is attached to the session; endpoints enforce it server-side. See Roles & permissions for the capability matrix.

Sign in with email/password at POST /api/auth/sign-in/email. The response’s set-auth-token header contains the bearer session. The native app stores it in Expo SecureStore and sends Authorization: Bearer <session-token> on subsequent calls. POST /api/auth/sign-out revokes it.

Reports, updates, and push subscriptions belong to the actual signed-in account. Protected data and writes require membership of the property’s organization; staff roles do not grant access across organizations. Null organization IDs never grant membership.

X-Property-Id can select a property for API requests. Without a selection, signed-in users default to their organization’s first property. Public status data remains public; contacts and invoices do not.

The legacy shared MOBILE_API_KEY is accepted only in a dedicated demo with DEMO_MODE=true. It is disabled by default and is not used by the mobile app’s sign-in flow.

CRON_SECRET guards exactly two endpoints - the SLA escalation sweep and the weekly digest:

Terminal window
curl -X POST -H "Authorization: Bearer ${CRON_SECRET}" \
http://localhost:3000/api/cron/sweep
curl -X POST -H "Authorization: Bearer ${CRON_SECRET}" \
http://localhost:3000/api/cron/digest

Actions these trigger are audited as the system actor. Scheduling recipes (crontab and n8n) are in SLA policy & escalation and Weekly digest. Both jobs can also run without the HTTP layer at all: npm run sweep / npm run digest connect to the database directly.

INBOUND_EMAIL_SECRET guards POST /api/inbound-email, the provider-agnostic email-ingestion endpoint. Configure your email provider/forwarder to POST parsed messages there with the bearer header; the payload format and [INC-<id>] subject behavior are documented in Admin & ops and Triage queue & email-in.

Strictly the reverse direction - CondoPulse authenticating itself to your systems. Every outgoing webhook delivery is signed with the hook’s secret:

x-condopulse-signature: sha256=<HMAC-SHA256(raw body, secret)>

Verification snippet and details: Webhooks.

  • Missing/wrong bearer key or secret → 401.
  • Valid identity, insufficient role (e.g. resident calling a staff endpoint, or staff requesting another organization’s private data) → 403.
  • The cron and inbound-email secrets are not interchangeable with each other or with MOBILE_API_KEY - each bearer value works only on its own endpoints.
Terminal window
DEMO_MODE=false # shared demo-key compatibility disabled
CRON_SECRET=dev-cron-secret # /api/cron/*
INBOUND_EMAIL_SECRET=... # /api/inbound-email
BETTER_AUTH_SECRET=... # session signing - openssl rand -hex 32