nivå

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

  1. The owner creates a token on Team → Settings → API access with the scopes read:aggregates and read:model, and copies the secret once.
  2. 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.
  3. Call GET /orgs/{id}/trend for monthly averages, and GET /model/niva-sdlc for the area names and level definitions in both languages.
  4. Refresh no more than every few minutes; the picture changes when people submit, not by the second.

An HR system in sync

  1. 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.
  2. Match on email. A person who left has `leftAt` set and stays in the list for the organisation's records.
  3. 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.
  4. Writing members into nivå (provisioning) arrives in a later phase.

A person's own client

  1. Send the person's Supabase session JWT as the Bearer token; the API then sees exactly what that person sees on the web.
  2. 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.
  3. 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 } }
  ]
}