Log in with username and password
POST/api/login
loginWithPassword
Validates local credentials. Returns either a session cookie (when no TOTP is configured) or a 200 `LoginStep::TotpChallenge` body the caller must satisfy via `/api/auth/login/challenge/totp`.
Authentication is not described for this operation in the spec — that does not mean it is public. Check tokens and scopes.
curl
curl -X POST 'https://neosource.dev/api/login' \
-H 'Content-Type: application/json' \
-d '{"handle":"dev","password":"dev"}'fetch
fetch("https://neosource.dev/api/login", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({"handle":"dev","password":"dev"}),
});Request bodyrequired
application/json
Responses
200Secondary factor required
application/json
201Session created and cookie set
application/json
Standard errors
Bodies documented once for the whole API — see standard errors.
- 400Invalid request body
- 401Invalid credentials
- 429Rate limited — retry after the interval in the `Retry-After` header
- 500Internal server error
- 503Service temporarily unavailable / at capacity — retry after the `Retry-After` header
- 504Gateway timeout — the request exceeded the server's handling budget
Schemas
Referenced above. Listed here rather than expanded inline, so the same definition is not repeated at every level.
ViewerWorkspace
One workspace the viewer belongs to, in the shape the SPA's workspace switcher needs at first paint. Deliberately leaner than [`OrgResponse`]: no `description` / `created_at` / `updated_at` — this rides `/api/user/me`, which every authenticated page fetches, so it pays for bytes on the app's hottest request. `/api/orgs` keeps the full shape for CLI/API consumers.
default_team_idThe default team's id, beside its key — because the key addresses a URL while every team-scoped read is id-keyed. The rail's tier-0 Cycles row labels itself from `GET /api/teams/{id}/cycles`, and without this it would have to resolve the key first: two hops for a label, on the page this field exists to make paint in one. See [`ViewerWorkspace::team_count`] — same UX-hint status.
default_team_keySee [`ViewerWorkspace::team_count`].
display_namerequiredkindrequiredslugrequiredteam_countrequiredHow many teams this workspace has, and the `key` of its default team. These two exist so ADR 0077 §3's team-disclosure tier is known at FIRST PAINT. The tier is `team_count >= 2`, and every surface that has to know — the rail's Teams group, its tier-0 Board/Cycles rows, the global New ticket page's picker — used to learn it from a second round trip (`/api/workspaces/{slug}/teams`) or, worse, from a remembered count in localStorage that could be a different account's. Riding this request is rung 1 of docs/frontend-design.md's loading ladder: `/api/user/me` is fetched by every authenticated page and prefetched at HTML-parse time, so the tier arrives in the frame the rail first paints. A **UX hint only**, exactly like `viewer_role` above: what a viewer may see of a team stays server-side in `PolicyAuthzService`. A member of the workspace who is not on the team still gets the rows, and the surfaces behind them render their own not-a-member state (ADR 0077 §6). `default_team_key` is `None` when the workspace has no team at all, and also for a legacy workspace whose teams predate default-provisioning.
viewer_rolerequiredworkspace_idrequired