Get started
Authentication
Two credentials. An API key exercises actions; an OAuth token, granted by a person, exercises actions and reads. Both carry scopes with a version, so a credential keeps opening exactly what it opened the day it was issued.
Last updated
API keys
A key belongs to the workspace, not to a person: it authenticates an integration. Send it as a bearer token:
Authorization: Bearer nk_…It is born scoped and with an expiry, and it is shown once, when created. Revoking it is one click; it stops working immediately. A key is never returned by any endpoint: if you lose it, you issue another.
Scopes carry the version
A scope names one action and one version: crm.call_logged@1. A new version of an action is a new scope, so nothing changes under a credential that was not issued for it.
miira.lead_create@1
crm.call_logged@1
crm.owner_assigned@1
crm.stage_moved@1OAuth (MCP and AI clients)
The MCP server uses OAuth 2.1 with dynamic client registration and PKCE. Discovery lives at the standard address:
https://niiko.org/api/mcp
https://niiko.org/.well-known/oauth-authorization-serverAuthorization always goes through a niiko screen where the person picks one workspace and which actions and reads to grant — none checked by default. The resulting token is a credential of the actions and reads endpoints, with those scopes. Access is revoked from Settings → Developers → Apps.
Two permissions, not one
Besides the credential, every action and every read needs the workspace owner to have switched it on (Settings → Connections, column “API & MCP”). The reason is deliberate: whoever holds a key and whoever owns the workspace are usually two different people, and each one gets to decide.
Reads: OAuth only, with the person's role
A read is done on behalf of the person who authorized the app, and answers with what that person can see on their own screen. An API key has no person behind it, so the reads endpoint refuses it with reads_need_a_person without even checking it.
POST /api/v1/reads/kiipu.collections_get
Authorization: Bearer nk_…
403 { "status": "refused", "reason": "reads_need_a_person", "detail": { "message": "…" } }Then three checks, in order: the token's scope (kiipu.collections_get@1), the person's role (role_forbidden when their screen does not show it) and the workspace switch (grant_off). Someone who leaves the workspace stops reading, even if their token is still alive.
Why 401 says so little
A nonexistent, revoked, expired or under-scoped credential all answer the same unauthorized. Telling them apart would turn the endpoint into an oracle for whoever is probing keys. Your key's exact state is in your dashboard.