Authentication
Every binary accepts two credentials, and which one a route accepts is part of its contract. Both are sent the same way:
Authorization: Bearer <credential>
Session tokens
A signed JWT, issued by POST /api/auth/login and by the signup verification
endpoint. This is what the web console uses.
The token carries only two claims — the user id and the organization id:
{ "uid": 4, "org_id": 2, "exp": 1789459200 }
Everything else about the caller — their role, their email, whether they must change their password — is read from the database on each request. That is deliberate: a role change or an org approval takes effect immediately rather than at the next token issuance.
The token lifetime is set per deployment when the issuer is constructed.
API keys
For server-to-server integration. Every key starts with nn_k_:
nn_k_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Keys are stored hashed. The plaintext is shown exactly once, at creation. See API keys for issuing and revoking them.
A key carries scopes; a session carries a role. They are not interchangeable, and a request never carries both credentials.
Roles
Three org roles, ranked. A check asks for "manager or above" rather than listing acceptable roles.
| Role | Intended for |
|---|---|
admin | Runs the account — team, API keys, org identity, telephony credentials |
manager | Day-to-day configuration, but cannot change who has access |
member | Day-to-day work without changing how anything is configured |
Superadmin is not in this list. It is a platform-level capability derived
from the caller's email domain (SUPERADMIN_EMAIL_DOMAINS / SUPERADMIN_EMAIL),
checked separately. A superadmin bypasses RequireRole entirely and can list
and enter any organization on the deployment.
admin, manager, and member are the roles the authorization layer
recognises. Any other value carries no privileges and is refused by every
role-gated route.
A member can never create an account above their own role.
Scopes
Scopes are app-specific vocabulary. Nothing in any app's scope list grants member management, telephony credentials, or key issuance — those stay behind a human admin session on purpose.
nextneural_converse
| Scope | Allows |
|---|---|
calls:read | List calls, read transcripts and statistics |
calls:write | Trigger and modify calls |
customers:read | List and read contacts |
customers:write | Create, update, and delete contacts |
flows:read | List campaigns and inbound flows |
flows:write | Create and modify flows |
webhooks:read | List webhooks and delivery logs |
webhooks:write | Register and delete webhooks |
nextneural_assist
| Scope | Allows |
|---|---|
customers:read / customers:write | Read / manage customers |
calls:read / calls:write | Read live calls / start calls |
agents:read | List agents |
kb:read / kb:write | Read / manage knowledge base documents |
nextneural_intel
| Scope | Allows |
|---|---|
sales:analyses:read | Read submitted analyses and field labels |
sales:analyses:write | Submit analyses |
sales:simulations:judge | Score a simulated run (used by nextneural_coach) |
nextneural_coach
Keys authenticate against the same routes as a session. Role-gated routes
(manager and admin actions such as creating personas or scenarios) refuse
key credentials outright.
Choosing a credential
| You are | Use |
|---|---|
| The web console, or a script acting as a person | Session token |
| A backend service, CRM sync, or scheduled job | API key |
| Performing an admin action (members, keys, telephony) | Session token — keys are refused |
Rejections
| Status | Body | Cause |
|---|---|---|
401 | {"detail": "missing bearer token"} | No Authorization header |
401 | {"detail": "invalid or expired token"} | Bad or expired session JWT |
401 | {"detail": "invalid or revoked API key"} | Key not found or revoked |
401 | {"detail": "this endpoint requires a signed-in user session, not an API key"} | Key sent to a session-only route |
403 | {"detail": "this action requires the admin role"} | Role too low |
403 | {"detail": "this action requires a signed-in admin, and is not available to API keys"} | Key sent to a role-gated route |
403 | {"detail": "insufficient scope", ...} | Key lacks the scope the route requires |
403 | {"detail": "organization setup is required before using the app", "needs_org_setup": true} | Account has no organization |
403 | {"detail": "your organization is awaiting approval", "needs_waitlist_approval": true} | Organization is pending or rejected |
403 | {"detail": "set your own password before using the app", ...} | Invited user has not set a password |
See Errors for the full list and the response shape.
WebSocket authentication
Browser WebSocket clients cannot set an Authorization header, so live-stream
endpoints take the token as a query parameter instead:
wss://your-host/api/voice/ws/live/<call_uuid>?token=<session-token>
The same applies to a handful of media endpoints loaded by plain <audio> and
<img> tags, such as nextneural_intel's recording playback, which accept the token
in either the header or the query string.