Create or refresh a browser session
POST/api/sessions
createSession
Issues a `ns_session` cookie for an authenticated caller with an existing `ns_session` cookie. Any bearer PAT, including unrestricted and legacy tokens, is refused with 403: the session it would mint carries no grant, so the exchange would widen the credential rather than narrow it (ADR 0069).
Requires authentication using a session cookie — see tokens and scopes.
curl
curl -X POST 'https://neosource.dev/api/sessions' \
-b 'ns_session=$NEOSOURCE_SESSION'fetch
fetch("https://neosource.dev/api/sessions", {
method: "POST",
credentials: "include",
});Responses
201Session created and cookie set
application/json
403Forbidden — one of: forbidden, needs_scope
application/json
Standard errors
Bodies documented once for the whole API — see standard errors.
- 400Bad Request — one of: invalid_input
- 401Authentication required
- 404Not Found — one of: not_found
- 409Conflict — one of: already_exists, conflict
- 429Rate limited — retry after 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