neosourceDocs
Search docs

Update the current user's account settings

PATCH/api/user/me

updateCurrentUser

Session-authenticated caller only. Patches the authenticated account's own settings. Absent fields are left unchanged.

Requires authentication using a session cookie — see tokens and scopes.

curl

curl -X PATCH 'https://neosource.dev/api/user/me' \
  -b 'ns_session=$NEOSOURCE_SESSION' \
  -H 'Content-Type: application/json' \
  -d '{}'

fetch

fetch("https://neosource.dev/api/user/me", {
  method: "PATCH",
  headers: {
    "Content-Type": "application/json",
  },
  credentials: "include",
  body: JSON.stringify({}),
});

Request bodyrequired

application/json

UpdateUserRequest

object

Patch the authenticated account's own settings. Every field is optional; an absent field is "leave it alone", which is why a bare `bool` would be wrong here.

commit_email_private

boolean | null

additional propertiesnot allowed

Responses

200Updated user info

application/json

UserResponse

object

commit_email_privaterequired

boolean

display_namerequired

string

email

string | null

email_verified_at

integer | nullint64

Millis-since-epoch when the email was provably verified (OAuth `email_verified=true` at link time, or the email- verification flow in `plans/auth-flows.md` §"Email verification"). NULL = unverified — SPA renders a banner.

handlerequired

string

linked_providers

array

OAuth provider slugs (e.g. `"github"`) linked to this account. Populated only on the self endpoint (`/api/user/me`) so the SPA knows link state at first paint (onboarding cards, settings); public profile responses omit it — another user's linked providers are not public information.

items

string

password_changed_at

integer | nullint64

Millis-since-epoch when the password hash was last set or rotated. `None` for OAuth-only accounts that have never set a password. Drives the "Password last changed N days ago" copy on `/settings/security` (plans/auth-flows.md slice #4).

totp_enabledrequired

boolean

user_idrequired

string

workspaces

array | null

Every workspace this account belongs to — personal first-class, orgs included — with the viewer's role in each. Populated only on the **self** surfaces (`/api/user/me` and the session-creating responses); the public profile (`/api/users/{handle}`) omits it, since another user's membership is not public. Also absent for a resource-narrowed token (`TokenGrant::permits_account` false) — a workspace-locked token must not enumerate every workspace the owning account can reach. It rides the viewer payload rather than sitting behind `/api/orgs` because the SPA's workspace switcher cannot paint without it: which workspace the URL's `:owner` denotes is a *membership* question, and answering it one round trip after the viewer resolves is what made the chip commit to the personal workspace and then flip to the org (docs/frontend-design.md §"Ride the boot path", issue #15). `Option`, not a bare `Vec`: absent ("this response does not carry membership" → the client falls back to `/api/orgs`) and `[]` ("confirmed: no workspaces") must stay distinguishable. Collapsing them is exactly what would make the client paint the personal workspace for an org member. See plans/viewer-workspace-membership.md §4.

items

ViewerWorkspace

object

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_id

string | null

The 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_key

string | null

See [`ViewerWorkspace::team_count`].

display_namerequired

string

kindrequired
WorkspaceKind
slugrequired

string

team_countrequired

integerint32

How 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_rolerequired
WorkspaceRole
workspace_idrequired

string

403Forbidden — one of: forbidden, needs_scope

application/json

one of
  • ErrorForbidden
  • NeedsScopeError

    object

    `403` body returned when listing private repos but the linked identity lacks the required provider scope. The SPA turns this into an incremental-authorization prompt (Tier 2) that calls the `/elevate` OAuth endpoint with this `scope`.

    errorrequired

    string

    Always `needs_scope` — this body exists to carry the extra fields that kind needs.

    "needs_scope"

    scoperequired

    string

    The provider scope to request via elevation (e.g. `"repo"`).

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.

WorkspaceKind

string

The kind of workspace (namespace container).

"personal""organization"

WorkspaceRole

string

Workspace membership role. Ordered low → high (`Member < Admin < Owner`) so that `if actor_role >= WorkspaceRole::Admin` reads naturally.

"member""admin""owner"