Accounts
Signup, login, password reset, and profile. These routes are identical across all four binaries; only the path prefix differs.
| Binary | Prefix |
|---|---|
nextneural_converse, nextneural_intel, nextneural_assist | /api/auth |
nextneural_coach | /api/nn-coach/auth |
Paths below use /api/auth.
Auth configuration
GET /api/auth/config — no credential required.
Tells a client what sign-in methods this deployment offers, so the login page can render the right buttons.
{
"firebase_enabled": true,
"deployment_mode": "cloud"
}
Signup
Signup is two calls. The account does not exist after the first one — the submitted details are held until the emailed code is confirmed.
Start signup
POST /api/auth/signup
{
"email": "[email protected]",
"password": "a-strong-password",
"first_name": "Priya",
"last_name": "Kumar"
}
202 Accepted:
{ "detail": "We've emailed you a code — enter it to finish creating your account." }
A second attempt with the same address replaces the pending signup rather than creating a duplicate, which is what makes "resend" safe.
| Status | Cause |
|---|---|
400 | A required field is missing or malformed (nextneural_intel only — see below) |
409 | An account with this email already exists |
nextneural_intel validates at the binding layer: email must be a well-formed
address and password must be at least 8 characters, both rejected with 400
before any work happens. nextneural_converse and nextneural_coach declare no binding
constraints on this route.
Verify the code
POST /api/auth/verify-signup-otp
{ "email": "[email protected]", "code": "428913" }
201 Created — this is the call that creates the account, and it returns a
session immediately:
{
"token": "eyJhbGciOiJIUzI1NiIs...",
"user": {
"id": 4,
"uuid": "dac0309e-d46d-4b48-a420-05d976e64c5c",
"email": "[email protected]",
"first_name": "Priya",
"last_name": "Kumar",
"role": "admin",
"mobile_number": null,
"phone_number": null,
"profile_picture": null,
"organization": null,
"must_change_password": false,
"has_password": true,
"superadmin": false
}
}
| Status | Cause |
|---|---|
400 | {"detail": "that code is incorrect or has expired — request a new one"} |
429 | {"detail": "too many incorrect attempts — request a new code"} |
409 | An account with this email already exists |
Wrong-code attempts are counted per pending signup and locked out well before a six-digit space could be exhausted.
Resend the code
POST /api/auth/resend-signup-otp
{ "email": "[email protected]" }
Always returns 200 with the same body whether or not a signup is pending —
this does not reveal whether an address is registered:
{ "detail": "If a signup is pending for that address, a new code is on its way." }
Login
POST /api/auth/login
{ "email": "[email protected]", "password": "a-strong-password" }
200 OK — same {token, user} shape as verification above.
| Status | Cause |
|---|---|
401 | {"detail": "incorrect email or password"} — returned for both a wrong password and an unknown address |
Google sign-in
POST /api/auth/firebase
Registered only when the deployment has Firebase configured. Where it is
not, the route does not exist and returns 404 — which is what
GET /api/auth/config reports via firebase_enabled.
{ "id_token": "<Google ID token>", "intent": "signin" }
| Field | Required | Notes |
|---|---|---|
id_token | ✓ | The Google ID token |
intent | — | signin or signup. Omitted means either is acceptable |
Exchanges a Google ID token for a session in the same {token, user} shape. An
account created this way has no password: its has_password is false, which
is what lets the UI offer to create one rather than confirm an existing one.
Password reset
Request a reset
POST /api/auth/forgot-password
{ "email": "[email protected]" }
Always 200, regardless of whether the address exists. The emailed link carries
a token; only its hash is stored, so a leaked database does not let anyone
complete someone else's reset.
Complete the reset
POST /api/auth/reset-password
{ "token": "<token from the email>", "new_password": "a-new-password" }
The signed-in account
All of these require Authorization: Bearer <session-token>. They work
before organization setup is complete — that is the point, since a fresh
account needs /auth/me to discover that setup is still pending.
Current user
GET /api/auth/me
Returns the same user object shown above. The organization field is an
object or null, never omitted: a client gates the whole app on it being
null.
Log out
POST /api/auth/logout
Change password
POST /api/auth/change-password
{ "current_password": "...", "new_password": "..." }
new_password must be at least 8 characters.
An account that has no password at all — one created by Google sign-in —
sets its first password here without supplying current_password. Reaching
this endpoint already required a valid session, which is the same bar as
knowing the old password.
An invited user does have a password (the generated one that was emailed to them) and must supply it. Skipping that check would let whoever holds the session take the account over silently.
| Status | Cause |
|---|---|
400 | {"detail": "the new password must be at least 8 characters"} |
401 | {"detail": "current password is incorrect"} |
403 | Sent with an API key — this route requires a signed-in user |
Update profile
PATCH /api/auth/profile
{ "first_name": "Priya", "last_name": "Kumar", "mobile_number": "+919876543210" }
Avatar
POST /api/auth/profile/picture — multipart upload
DELETE /api/auth/profile/picture — remove
In nextneural_intel, the image itself is served from
GET /api/auth/profile/picture, which accepts the token in the header or
as a ?token= query parameter, because an <img src> cannot set a header.
Invited users
A member created by an admin (see Organizations)
is issued a temporary password and flagged must_change_password: true. Until
they set their own, every org-scoped route refuses them:
{ "detail": "set your own password before using the app", "must_change_password": true }
The only routes that stay open are the account routes above — enough to log in
and call POST /api/auth/change-password.