---
title: Users
description: Reading your userbase, and what a person may change themselves.
---

import Endpoint from '../../../components/Endpoint.astro';
import Callout from '../../../components/Callout.astro';

Two different things live here, and the difference is who is asking.

## Your userbase: secret key

<Endpoint method="GET" path="/v1/users" />
<Endpoint method="GET" path="/v1/users/{userId}" />

Your administrative view. These read **anybody**, so they take a secret key and
must be called from your server.

## The signed-in person: session token

<Endpoint method="GET" path="/v1/me" />

Returns the caller, the organizations they belong to, and which one the session
is currently acting as.

```json
{
  "user": { "id": "user_…", "email": "ada@example.com", "firstName": "Ada", "…": "…" },
  "organizations": [
    { "id": "org_…", "name": "Acme", "slug": "acme", "role": "admin", "imageUrl": null }
  ],
  "organization": null
}
```

<Callout type="info">
There is no id in the path, and that is deliberate: a path parameter invites an
authorisation check that somebody eventually forgets. With no parameter there
is nothing to forget.
</Callout>

The `organizations` list is what a switcher renders. `switch-organization` can
change the active one, but nothing else can tell you what the choices are.

### Update your own profile

<Endpoint method="PATCH" path="/v1/me" />

```json
{ "firstName": "Ada", "lastName": "Lovelace", "username": "ada" }
```

`imageUrl` must be an https URL. Send `null` to remove the picture; leaving the
field out leaves it alone.

<Callout type="warning">
`publicMetadata` is **not** editable here. It travels inside the access token
and your backend trusts it for authorisation. A user who could write it could
set `{"role":"admin"}` and hand it to a server that believes tokens. It stays
writable only through the secret-key API, where you decide.

`email` is not editable either. Changing an address has to be proved before it
takes effect, or anybody who finds an unlocked laptop moves the account
somewhere they control.
</Callout>

### Ask for an upload URL

<Endpoint method="POST" path="/v1/me/assets/upload-url" />

```json
{
  "purpose": "user_avatar",
  "contentType": "image/png",
  "contentLength": 84213,
  "sha256": "e3b0c44298fc1c14…",
  "width": 512,
  "height": 512
}
```

Returns a short-lived signed URL to `PUT` the bytes to, plus the `publicUrl` to
save afterwards with `PATCH /v1/me`. The file never passes through this API.

`sha256` is the hex digest of the file and becomes the object key, so uploading
the same picture twice costs one upload: the second response comes back with
`alreadyExists: true` and no `uploadUrl`, and you save the `publicUrl` directly.

PNG, JPEG and WebP, up to 1 MB. SVG is refused: an SVG is a document that can
carry a script, and these images render on sign-in screens.

`purpose` may be `user_avatar` or `organization_image`. The second needs the
`org:manage` permission on the organization, checked against the live
membership. Branding assets belong to you rather than to your users and are
uploaded from the console, not from here.

<Callout>
When object storage is not configured on the deployment, this returns a 400
saying so. Every image field still accepts an https URL you host yourself,
which is what they did before uploads existed.
</Callout>

### Change your password

<Endpoint method="POST" path="/v1/me/password" />

```json
{ "currentPassword": "…", "newPassword": "…" }
```

The current password is required even though the session already proves who
signed in. A session is proof that somebody signed in, not that the person at
the keyboard right now is the same one.

Every **other** session is revoked. The one making the change survives.

```json
{ "changed": true, "otherSessionsRevoked": 3 }
```