Neutrino Docs
API ReferenceIam

NNO IAM Service API

Identity and Access Management — authentication, roles, permissions, API keys, and service tokens.

The IAM service exposes two route families: routes documented by the OpenAPI spec below (operator endpoints, OAuth device flow, organization invitations, profile, internal hooks), and additional surfaces mounted by Better Auth and the user-facing org-setup endpoint that are documented manually in the next section.

Manually Documented Surfaces

The endpoints below are mounted on the IAM worker but are not enumerated by the @hono/zod-openapi document because they are either delegated to a third-party handler (Better Auth) or do not declare an OpenAPI route definition. Treat this section as authoritative for those surfaces.

Better Auth (/api/auth/*)

Mounted by createAuthApp() in services/iam/src/core/index.ts. The handler is the standard Better Auth Hono integration with the organization, magicLink, and apiKey plugins enabled. Common endpoints include:

MethodPathDescription
POST/api/auth/sign-up/emailEmail/password sign-up.
POST/api/auth/sign-in/emailEmail/password sign-in. Returns a session token + cookie.
POST/api/auth/sign-in/magic-linkIssues a magic-link email via Resend.
GET/api/auth/magic-link/verifyVerifies a magic-link token and creates the session.
POST/api/auth/sign-outRevokes the current session.
GET/api/auth/get-sessionReturns the current Better Auth session.
POST/api/auth/organization/*Better Auth organization plugin (create, list, set-active, members, invitations).

Additional auth-adjacent routes mounted alongside Better Auth:

MethodPathDescription
POST/api/validate-sessionService-to-service session validation. Used by the gateway and the registry.
GET/api/audit/*Audit log read endpoints (authentication and authorization events).
GET/api/sessionsList the caller's active sessions.
DELETE/api/sessions/:idRevoke a specific session.
GET/api/nno/sessionEnriched NNO session — adds permissions, tenant context, and effective role on top of the Better Auth session.
GET/healthHealth check. Returns { status: "healthy" | "degraded", checks: {...} }.

For the canonical schema of each Better Auth endpoint, see the upstream documentation; the IAM worker does not modify request or response shapes.

POST /api/nno/org-setup

User-facing organization + platform creation, called from the console "Create Organization" page.

  • Auth: requires a Better Auth session cookie (no Bearer token path).

  • Body:

    { "name": "string (1-100)", "slug": "string (1-63, ^[a-z0-9-]+$)" }
  • Behaviour: creates the IAM organization (Better Auth organization plugin) and the matching Registry platform atomically. If the Registry call fails, the IAM organization is rolled back.

  • Response: 201 with { orgId, platformId, slug, name } on success. 400 for validation errors, 401 if no session, 409 if the slug is taken.

Source of truth: services/iam/src/routes/nno-org-setup.ts.

List role→permission mappings for an organization

GET
/api/nno/roles

Query Parameters

orgId*string
Length1 <= length

Response Body

application/json

application/json

curl -X GET "https://iam.svc.nno.app/api/nno/roles?orgId=string"
null
null

Upsert a role→permission mapping for an organization

POST
/api/nno/roles

Request Body

application/json

orgId*string
Length1 <= length
role*string
Length1 <= length
permissions*array<string>
Items0 <= items

Response Body

application/json

application/json

application/json

curl -X POST "https://iam.svc.nno.app/api/nno/roles" \  -H "Content-Type: application/json" \  -d '{    "orgId": "string",    "role": "string",    "permissions": [      "string"    ]  }'
null
null
null

Remove a role→permission mapping

DELETE
/api/nno/roles/{orgId}/{role}

Path Parameters

orgId*string
role*string

Response Body

application/json

curl -X DELETE "https://iam.svc.nno.app/api/nno/roles/string/string"
Empty
null

List permission grants for an organization

GET
/api/nno/grants

Query Parameters

orgId*string
Length1 <= length
userId?string

Response Body

application/json

application/json

curl -X GET "https://iam.svc.nno.app/api/nno/grants?orgId=string"
null
null

Create a per-user permission grant or denial

POST
/api/nno/grants

Request Body

application/json

userId*string
Length1 <= length
orgId*string
Length1 <= length
permission*string
Length1 <= length
granted*boolean
grantedBy*string
Length1 <= length
expiresAt?integer
Range0 < value < 100000000000

Response Body

application/json

application/json

curl -X POST "https://iam.svc.nno.app/api/nno/grants" \  -H "Content-Type: application/json" \  -d '{    "userId": "string",    "orgId": "string",    "permission": "string",    "granted": true,    "grantedBy": "string"  }'
null
null

Remove a permission grant

DELETE
/api/nno/grants/{id}

Path Parameters

id*string

Response Body

application/json

curl -X DELETE "https://iam.svc.nno.app/api/nno/grants/string"
Empty
null

Create a Better Auth organization for a tenant

POST
/api/nno/organizations

Request Body

application/json

name*string
Length1 <= length <= 100
slug*string
Match^[a-z0-9-]+$
Length1 <= length <= 63
ownerId*string
Length1 <= length
orgType?string
Default"tenant"
Value in"nno-operator" | "tenant"

Response Body

application/json

application/json

application/json

application/json

curl -X POST "https://iam.svc.nno.app/api/nno/organizations" \  -H "Content-Type: application/json" \  -d '{    "name": "string",    "slug": "string",    "ownerId": "string"  }'
null
null
null
null

Look up an organization by slug

GET
/api/nno/organizations/{slug}

Path Parameters

slug*string

Response Body

application/json

application/json

curl -X GET "https://iam.svc.nno.app/api/nno/organizations/string"
null
null

Bootstrap the NNO operator org

POST
/api/nno/bootstrap

Response Body

application/json

application/json

application/json

application/json

curl -X POST "https://iam.svc.nno.app/api/nno/bootstrap"
null
null
null
null

Validate a raw API key

POST
/api/nno/apikey/validate

Request Body

application/json

key*string
Length1 <= length

Response Body

application/json

application/json

application/json

curl -X POST "https://iam.svc.nno.app/api/nno/apikey/validate" \  -H "Content-Type: application/json" \  -d '{    "key": "string"  }'
null
null
null

Issue a short-lived service-to-service JWT

POST
/api/nno/service-token

Request Body

application/json

serviceId*string
Length1 <= length
targetService*string
Length1 <= length

Response Body

application/json

application/json

application/json

application/json

curl -X POST "https://iam.svc.nno.app/api/nno/service-token" \  -H "Content-Type: application/json" \  -d '{    "serviceId": "string",    "targetService": "string"  }'
null
null
null
null

Validate CLI session token and return identity

POST
/api/v1/cli/whoami

Response Body

application/json

application/json

curl -X POST "https://iam.svc.nno.app/api/v1/cli/whoami"
{
  "email": "string",
  "platformId": "string",
  "role": "string",
  "expiresAt": "string"
}
{
  "error": {
    "code": "string",
    "message": "string",
    "requestId": "string"
  }
}

Request a device authorization code (RFC 8628 §3.2)

POST
/oauth/device/code

Request Body

application/json

client_id*string
Value in"nno-cli"

Response Body

application/json

curl -X POST "https://iam.svc.nno.app/oauth/device/code" \  -H "Content-Type: application/json" \  -d '{    "client_id": "nno-cli"  }'
{
  "device_code": "string",
  "user_code": "string",
  "verification_uri": "string",
  "expires_in": 0,
  "interval": 0
}

Authorize a pending device code (called by nno.app/activate)

POST
/oauth/device/authorize

Request Body

application/json

user_code*string
Length1 <= length

Response Body

application/json

application/json

application/json

application/json

curl -X POST "https://iam.svc.nno.app/oauth/device/authorize" \  -H "Content-Type: application/json" \  -d '{    "user_code": "string"  }'
{
  "ok": true
}
null
null
null

Poll for the device access token (RFC 8628 §3.5)

POST
/oauth/device/token

Request Body

application/json

grant_type*string
Value in"urn:ietf:params:oauth:grant-type:device_code"
device_code*string
client_id*string
Value in"nno-cli"

Response Body

application/json

curl -X POST "https://iam.svc.nno.app/oauth/device/token" \  -H "Content-Type: application/json" \  -d '{    "grant_type": "urn:ietf:params:oauth:grant-type:device_code",    "device_code": "string",    "client_id": "nno-cli"  }'
{
  "access_token": "string",
  "token_type": "bearer",
  "expires_in": 0,
  "refresh_token": "string"
}

Rotate a CLI refresh token and issue a new access token (ADR-014)

POST
/oauth/token

Request Body

application/json

grant_type*string
Value in"refresh_token"
refresh_token*string
client_id*string
Value in"nno-cli"

Response Body

application/json

application/json

curl -X POST "https://iam.svc.nno.app/oauth/token" \  -H "Content-Type: application/json" \  -d '{    "grant_type": "refresh_token",    "refresh_token": "string",    "client_id": "nno-cli"  }'
{
  "access_token": "string",
  "token_type": "bearer",
  "expires_in": 0,
  "refresh_token": "string"
}
null

List pending invitations for an organization

GET
/api/nno/organizations/{orgId}/invitations

Path Parameters

orgId*string
Length1 <= length

Query Parameters

cursor?string
limit?integer
Default50
Range1 <= value <= 100

Response Body

application/json

application/json

application/json

curl -X GET "https://iam.svc.nno.app/api/nno/organizations/string/invitations"
{
  "data": [
    {
      "id": "string",
      "organizationId": "string",
      "email": "[email protected]",
      "role": "owner",
      "status": "string",
      "expiresAt": 0,
      "inviterId": "string",
      "createdAt": 0
    }
  ],
  "cursor": "string"
}
null
null

Invite a user to an organization

POST
/api/nno/organizations/{orgId}/invitations

Path Parameters

orgId*string
Length1 <= length

Request Body

application/json

email*string
Formatemail
role?string
Default"member"
Value in"owner" | "admin" | "member"
inviterId?string
Length1 <= length

Response Body

application/json

application/json

application/json

application/json

application/json

curl -X POST "https://iam.svc.nno.app/api/nno/organizations/string/invitations" \  -H "Content-Type: application/json" \  -d '{    "email": "[email protected]"  }'
{
  "id": "string",
  "organizationId": "string",
  "email": "[email protected]",
  "role": "owner",
  "status": "string",
  "expiresAt": 0,
  "inviterId": "string",
  "createdAt": 0
}
null
null
null
null

Revoke a pending invitation

DELETE
/api/nno/organizations/{orgId}/invitations/{invitationId}

Path Parameters

orgId*string
Length1 <= length
invitationId*string
Length1 <= length

Response Body

application/json

application/json

application/json

curl -X DELETE "https://iam.svc.nno.app/api/nno/organizations/string/invitations/string"
{
  "id": "string"
}
null
null

Update platform suspension status

POST
/internal/platform-status

Request Body

application/json

platformId*string
Length1 <= length
status*string
Value in"suspended" | "active"

Response Body

application/json

application/json

application/json

curl -X POST "https://iam.svc.nno.app/internal/platform-status" \  -H "Content-Type: application/json" \  -d '{    "platformId": "string",    "status": "suspended"  }'
null
null
null

Get the current user's profile

GET
/api/auth/profile

Response Body

application/json

application/json

application/json

curl -X GET "https://iam.svc.nno.app/api/auth/profile"
null
null
null

Update the current user's profile

PATCH
/api/auth/profile

Request Body

application/json

name?string
Length1 <= length
image?string|null
Formaturi

Response Body

application/json

application/json

application/json

curl -X PATCH "https://iam.svc.nno.app/api/auth/profile" \  -H "Content-Type: application/json" \  -d '{}'
null
null
null