Authentication
The API authenticates people with short-lived access tokens (JWTs) plus rotating refresh tokens, and servers with org-scoped API keys. Boards and public links use their own credentials — see Device tokens, public links & ingest.
Register
POST /auth/register
{
"name": "Alex Rivera",
"email": "alex@example.com",
"password": "a-strong-password",
"orgName": "Riverbend Dining"
}
Creates the user, and — when orgName is given — a new organization with the caller as owner, started on a 30-day Pro trial. Returns 201 with a full session (below). Registering an email that already exists returns 409. Limited to 3 registrations per IP per hour.
Log in
POST /auth/login
{ "email": "alex@example.com", "password": "a-strong-password" }
Or with curl:
curl -X POST https://miseline.keyedsystems.com/api/auth/login \
-H "content-type: application/json" \
-d '{"email":"alex@example.com","password":"a-strong-password"}'
Successful register/login/refresh responses share one shape:
{
"accessToken": "eyJhbGciOiJIUzI1NiJ9...",
"refreshToken": "wStB4nZk8mQ2vX...",
"expiresIn": 900,
"user": {
"id": "7f3a2c48-1b9e-4d2a-9c67-2f8e5a0d1b23",
"email": "alex@example.com",
"name": "Alex Rivera",
"memberships": [
{
"orgId": "05b955b6-c196-4e31-a383-4251dee16f8a",
"orgName": "Riverbend Dining",
"role": "owner",
"siteIds": []
}
]
}
}
accessToken— a JWT, valid for 15 minutes (expiresInis seconds). Send it on every request:Authorization: Bearer <accessToken>.refreshToken— valid for 30 days, single-use (see below).memberships— every organization the user belongs to, with their role and site scoping (siteIds: []means all sites). UseorgIdto build resource paths.
Five failed sign-in attempts for the same email+IP within 15 minutes locks that combination for 15 minutes (429 with Retry-After).
Refresh
POST /auth/refresh
{ "refreshToken": "wStB4nZk8mQ2vX..." }
Returns a fresh session and a new refresh token — the presented one is consumed. Store the replacement every time; reusing a consumed, expired, or revoked token returns 401. Changing the password revokes all outstanding refresh tokens (the change-password response includes a fresh session so the current client stays signed in).
Who am I
GET /auth/me
Returns the user object (with current memberships) for the presented access token — useful at app start and after membership changes.
Roles and permissions
A membership's role decides what the token can do in that organization. Roles are ranked — each includes everything below it:
owner > admin > manager > inspector (staff) > viewer
Writes check a named permission with a minimum role — for example creating recipes needs manageCulinary (manager+), inviting users needs manageUsers (admin+), and webhooks/API keys need manageOrg (owner). A role that's too low gets 403. Some clinical decisions additionally require a credential on the membership (e.g. diet signoffs require rd) regardless of rank. Site-scoped memberships (siteIds non-empty) are limited to those sites' data on site-scoped resources.
API keys
API keys are org-scoped server credentials for integrations — currently they authenticate the HL7 ADT ingest endpoint; everything else in the API expects a user session. Keys are available on Pro and Enterprise plans and managed by owners (in the app under Billing & plan → Developer, or via the API):
POST /orgs/{orgId}/api-keys
{ "name": "EHR interface" }
{
"id": "34c2927b-7efd-4725-ba1c-f67147787680",
"name": "EHR interface",
"key": "mk_live_mX9nVuFoKP_oEQrx7aG0Prfihgrdoo3W",
"note": "Store this now — it is shown once. Read-only scope; use header x-api-key."
}
The full key is returned once and stored hashed. Present it as the x-api-key header. GET /orgs/{orgId}/api-keys lists keys (id, name, prefix, last used, active); DELETE /orgs/{orgId}/api-keys/{id} revokes — revoked keys fail with 401 immediately.
Good citizenship
- Refresh proactively rather than retrying
401s in a loop. - Scope one API key per integration and name it honestly —
lastUsedAtand the prefix are your audit trail. - Treat refresh tokens and API keys like passwords: server-side storage only, never in client code or logs.