Authentication
How requests prove who is calling: a person's session, or an organisation API key.
Sessions
A person signs in through /api/auth (passkey, emailed link, or password with an optional authenticator code). The answer sets the session cookie __Secure-better-auth.session_token: HttpOnly, Secure, SameSite=Lax, valid for 7 days and renewed at most once a day while in use. Every /v1 request must carry it; without it the answer is 401 UNAUTHENTICATED.
An account must have verified its email before any /v1 call works (403 EMAIL_NOT_VERIFIED). Emailed sign-in links work once, for 5 minutes.
/api/auth accepts requests from the site's own origin only, which protects against cross-site request forgery.
The security check on public auth requests
These requests need a Cloudflare Turnstile token in the x-turnstile-token header, solved for the action shown. A token works once. Without a valid one the answer is 403 TURNSTILE_FAILED; if the check cannot run, 503 TURNSTILE_UNAVAILABLE. The site key for the widget comes from GET /api/public-config.
| Request | Turnstile action |
|---|---|
| POST /api/auth/sign-up/email | sign_up |
| POST /api/auth/sign-in/magic-link | sign_in_link |
| POST /api/auth/request-password-reset | password_reset |
| POST /api/auth/send-verification-email | verify_email |
Step-up for sensitive actions
Some actions (removing a member, delegating access, emergency access, overriding a medicine dose) also need a fresh proof of identity: an authenticator code or an emailed code exchanged for a short-lived proof, sent in the x-step-up header. Without it the answer is 403 STEP_UP_REQUIRED. The OpenAPI document marks each route that needs it.
Region header
You may send x-region with the region you expect (see Regions). If a different region answers, it refuses with 403 WRONG_REGION instead of handling the data.
API keys
An API key lets an agency's own software call the API. Agency admins who hold the users.manage function create keys on the Developers page in the ops app (/app/ops/developers), after a step-up check. An agency can hold up to 25 keys at once.
- Keys start
elv_live_on elavinya.com andelv_test_on the preview site and local copies. A key only works where it was made. - Each key lasts from 1 to 365 days; you choose when you create it.
- The key is shown once, when it is made or rotated. We store only a hash of it, so we cannot show it again. Keep it in a secret store.
- Rotate a key to get a new secret; the old one stops working. Revoke a key to stop it for good.
- A key acts for the person who made it, in that person's agency only. Each scope needs a function in that person's role, checked when the key is made and on every request, so a key can never do more than the person it acts for.
- After a rotation the key acts for the person who rotated it. Each scope then needs that person's function; a rotation by someone whose role lacks it is refused.
Send the key in the Authorization header:
curl https://elavinya.com/v1/roster/orgs/{orgId}/week \
-H "Authorization: Bearer elv_live_..."| Scope | What it allows | Function the creator needs |
|---|---|---|
| read:visits | Read the roster: visits, their times, carers and states. | roster.propose |
| write:visits | Create, assign, unassign, cancel and publish visits. | roster.publish |
| read:staff | Read staff skills, certificates and availability. | roster.publish |
| read:cases | Read clients' cases and care plans. | case.view-notes |
| read:medication | Read medicine charts and summaries. | medication.view |
| read:bookings | Read visit requests from families. | roster.publish |
| read:reports | Read operational reports. | reports.org |
| webhooks:manage | Read webhook endpoints and their deliveries, and send test events. | users.manage |
| Route | Scope needed |
|---|---|
| GET /v1/roster/orgs/:orgId/week | read:visits |
| GET /v1/roster/orgs/:orgId/board | read:visits |
| POST /v1/roster/orgs/:orgId/visits | write:visits |
| POST /v1/roster/orgs/:orgId/visits/publish | write:visits |
| POST /v1/roster/orgs/:orgId/visits/:visitId/assign | write:visits |
| POST /v1/roster/orgs/:orgId/visits/:visitId/unassign | write:visits |
| POST /v1/roster/orgs/:orgId/visits/:visitId/cancel | write:visits |
| GET /v1/roster/orgs/:orgId/staff | read:staff |
| GET /v1/cases/orgs/:orgId | read:cases |
| GET /v1/cases/:caseId | read:cases |
| GET /v1/cases/:caseId/plan | read:cases |
| GET /v1/medication/cases/:caseId/chart | read:medication |
| GET /v1/medication/cases/:caseId/summary | read:medication |
| GET /v1/bookings/orgs/:orgId/requests | read:bookings |
| GET /v1/reports/orgs/:orgId/reports/:kind | read:reports |
| GET /v1/reports/orgs/:orgId/reports/:kind/records | read:reports |
| GET /v1/integrations/orgs/:orgId/webhooks | webhooks:manage |
| POST /v1/integrations/orgs/:orgId/webhooks/:endpointId/test | webhooks:manage |
| GET /v1/integrations/orgs/:orgId/webhooks/:endpointId/deliveries | webhooks:manage |
Every other route answers 403 to a key: account, team, step-up, emergency access and key management stay with signed-in people. The org a request touches must be the key's own.
| Status | Code | When |
|---|---|---|
| 401 | UNAUTHENTICATED | The key is unknown, revoked or expired. |
| 403 | FORBIDDEN | The route is outside the key's scopes, is not open to keys, or belongs to another agency. |
| 429 | RATE_LIMITED | More than 120 per 1 minute for this key. Wait for the seconds in Retry-After. |