nivå API · v1
Read your organisation’s picture from your own tools
Read your organisation's picture from a BI tool or an HR system, or build a client on a person's own session. An org token never reads a person's answers or levels.
Base URL /api/v1. The contract is at /api/v1/openapi.json (OpenAPI 3.1). Errors are RFC 9457 problem details. Tokens and signed-in sessions are limited to 300 requests per minute each.
Endpoints
GET /
Name and version
GET /openapi.json
This document
GET /me
The signed-in person and their organisations
Session only.
GET /me/assessments
The person's assessments
Session only. The one in progress and the completed ones, newest first.
POST /me/assessments
Start an assessment
Session only. Body: { roleKey, rubricKey?, organisation? }. A member assesses their organisation's model in the role they were given; any assessment in progress is abandoned.
GET /me/assessments/{id}
One assessment
DELETE /me/assessments/{id}
Abandon an assessment in progress
PUT /me/assessments/{id}/areas/{key}
Answer one area
Body: { level: 1..5 }. Overwrites an earlier answer.
POST /me/assessments/{id}/complete
Finish the wizard
Every area must be answered. The result becomes a record; a confirmation email is sent.
POST /me/assessments/{id}/submit
Submit to the organisation
The completed assessment counts in the team picture from now on. Same words as the web: submit / withdraw.
POST /me/assessments/{id}/withdraw
Withdraw from the organisation
The assessment leaves the team picture again.
POST /me/assessments/{id}/retake
Retake from a completed assessment
A new assessment in progress with the previous answers carried over (same role), opened at areaKey. Body: { areaKey? }.
GET /me/tips
Guidance tips due next, for the phone to schedule
Session only. A schedule, not a queue: the same request twice gives the same answer, so a client can reschedule on every launch without a tip repeating. Dates already past are absent. `everyDays` is the person's own pace, null when they have turned tips off; `?organisation=` selects the lineage. `available` is false when the organisation does not hold Guidance, which is also when `tips` is empty -- a client showing a pace control should hide it rather than offer a setting that can lead nowhere.
POST /me/tips
Choose how often tips arrive
Session only. `{ everyDays }` — 1, 2, 3, 7 or 14, or null for never. The person's own setting: an organisation sets a review cadence, never how often somebody is helped.
POST /me/tips/{id}/done
A tip followed, and its steps done
Session only. No body: the id is the tip, taken from a step's `tip.placementId` on `/me/next-steps`. The one thing on a next step somebody can honestly finish -- a rung closes when the next assessment shows it moved, or when the level is raised explicitly. Idempotent: a second call answers 200 and changes nothing.
GET /me/next-steps
The next steps this person means to take
Session only. Open first, then completed. `?organisation=` selects the lineage; omit it for a personal plan. Each step carries `areaName` in the reader's language, resolved from the model version the step was chosen under -- a step survives a change of role, so a client that knows only the current role's areas cannot name it. An OPEN step also carries `tip`: the mechanism in a line, its four to six actions, and `at`/`of` among the tips placed at that coordinate. `tip` is null and `tipsExhausted` true when every tip there has been marked done; null with `tipsExhausted` false when none is placed yet, or when the organisation does not hold Guidance.
POST /me/next-steps
Take the step above the level reached in one area
Session only. `{ assessmentId, areaKey, fromLevel }`. The due date is the organisation's next review, not the caller's to set. Choosing again in the same area replaces the open step. **The 201 answers with the same body GET does** -- `organisation` and the full `nextSteps` array, `areaName` and `tip` included -- so a client that keeps the response has the tip in play rather than a step with nothing under it.
POST /me/next-steps/{id}
Finish or abandon a next step
Session only. `{ action: "done" | "drop" }`. A dropped step is never shown to anyone else.
GET /me/levels
The record: current level per area, and its history
Session only. `?organisation=` selects the lineage; omit it for the personal one. Theirs alone: no reading of anybody else's, under any setting.
POST /me/levels
Change one area directly, or publish the state as a snapshot
Session only. `{ action: "change", areaKey, level, note? }` -- lowering asks nothing, a note is kept on a raise only -- or `{ action: "publish" }`, which takes an ordinary shared snapshot from the state, never typed twice.
POST /me/devices
Register this phone for notifications
Session only. `{ token, environment, appVersion? }` — the APNs device token (64 hex characters), which of Apple's two gateways it belongs to, and the app's version. One row per token; registering again refreshes it.
DELETE /me/devices/{token}
Forget this phone
Session only, and only a token registered to the caller. The app calls it on signing out.
GET /me/blocks
The areas this person is blocked in
Session only. `?organisation=` is required: blocked is about the organisation's arrangements. Open blocks only.
POST /me/blocks
Say that an area is blocked by the organisation
Session only. `{ organisation, areaKey, reasonKind, reason? }` where reasonKind is policy, tooling, access, legal or other. One open block per area.
POST /me/blocks/{id}
It has cleared, or it is still in the way
Session only. `{ action: "cleared" | "confirm" }`. Only the person can say either; the organisation addresses, it never clears.
GET /orgs/{id}
One organisation
GET /orgs/{id}/modules
The organisation's plan
Every module on offer to the organisation, with what it holds, what is free while in preview, and what it can ask for.
POST /orgs/{id}/modules/{key}
Change one module on the plan
Owner only, and a person only. request adds a released module (requested, activated by nivå against the invoice) or a preview one by its audience (activated at once, free); ask asks to be let into a preview; cancel withdraws a request never activated; end asks nivå to end an active module, or ends a preview holding at once.
GET /orgs/{id}/members
Roster
Members, roles, teams, joined and left dates, whether each submitted this period. Never levels. Token scope read:roster; session: the owner.
GET /orgs/{id}/roles
Roles and targets
Token scope read:model.
GET /orgs/{id}/aggregates
The company picture per area
Averages from the same views the team page reads; an area with too few people behind it is withheld. Token scope read:aggregates; session: owner or manager.
GET /legal
Which version of each legal document this deployment serves
Public. The documents themselves ship inside the app, so a contract somebody accepted stays readable offline; this says when what they carry has been superseded. `updated` is an ISO date and is the version.
GET /orgs/{id}/trend
Company averages per area, per review period
One point per review period, oldest first. `month` is the first day of the period; `periodMonths` (1, 3, 6 or 12) says how long it is, following the organisation's review cadence. An organisation that has chosen no cadence reports monthly.
GET /model/{key}
The published version of a model
Both languages. Token scope read:model.
GET /model/{key}/{version}
One version of a model
Immutable once published: cache it forever. Supports If-None-Match.
Guides
A BI dashboard
- The owner creates a token on Team → Settings → API access with the scopes read:aggregates and read:model, and copies the secret once.
- Call GET /orgs/{id}/aggregates with `Authorization: Bearer niva_…`. Each area carries the company average, the respondent count, the target per role, and `withheld: true` when too few people are behind the number.
- Call GET /orgs/{id}/trend for monthly averages, and GET /model/niva-sdlc for the area names and level definitions in both languages.
- Refresh no more than every few minutes; the picture changes when people submit, not by the second.
An HR system in sync
- A token with read:roster reads GET /orgs/{id}/members: email, role, assessed role, team, joined and left dates, and whether each person submitted this period.
- Match on email. A person who left has `leftAt` set and stays in the list for the organisation's records.
- Levels are never in this response, by design: an HR record holding a level is performance data, and honest self-assessment depends on it not being one.
- Writing members into nivå (provisioning) arrives in a later phase.
A person's own client
- Send the person's Supabase session JWT as the Bearer token; the API then sees exactly what that person sees on the web.
- GET /me lists their organisations and assessed role. POST /me/assessments starts an assessment in the role they were given; PUT …/areas/{key} answers one area; POST …/complete finishes it; POST …/submit and …/withdraw share it with the organisation or take it back, in the same words as the web.
- Writes with the web's cookies are accepted only from this site; an app sends the JWT as a Bearer token and is not affected.
Example
curl -H "Authorization: Bearer niva_…" \
https://erniva.se/api/v1/orgs/ORG_ID/aggregates
{
"organisation": "ORG_ID",
"scope": "company",
"areas": [
{ "key": "planning", "name": "Planning & prioritisation",
"average": 3.2, "respondents": 7, "versions": 1, "withheld": false,
"targets": { "dev": 4, "qa": 4 } },
{ "key": "review", "name": "Review & critique",
"average": null, "respondents": 0, "versions": 0, "withheld": true,
"targets": { "dev": 4, "qa": 3 } }
]
}