# Users

> Create users under your own ids, look them up, merge and delete them, list their documents and set their check-ins.

A user is one of your people. Proof gives each user its own id, `usr_…`, and keeps your id for them beside it as `external_id`. Every other endpoint, event and seal names users by Proof id. Proof has no accounts and no sign-in: you create a user with a name, and from then on Proof credits what that user writes in the editor to that name. See [Users](/concepts/users/) for how the two ids, names, pictures and deletion work.

## The user object

| Attribute | Type | Description |
| --- | --- | --- |
| `id` | string | Proof's id for this user, `usr_` and 8 characters. It never changes. |
| `external_id` | string | Your id for this user, 1 to 128 characters, unique on your platform in each mode. |
| `name` | string | The name Proof shows on their writing, comments, cursors and seals. Up to 48 characters. |
| `avatar_url` | string or null | Proof's copy of their picture, or `null` for initials. |
| `detail` | string or null | A second line shown under their name where people pick someone to mention, such as an email address or a team. |
| `created_at` | timestamp | When you first created the user. |

```json
{
  "id": "usr_5Kq2Lm8R",
  "external_id": "u_42",
  "name": "Ada Lovelace",
  "avatar_url": "https://api.proof.exalto.ai/v1/avatars/av_9Rk3Tq",
  "detail": "ada@acme.com",
  "created_at": "2026-10-12T09:14:03Z"
}
```

## Create or update a user

`PUT /v1/users/external/{external_id}`

Creates a user for your id if there isn't one, and replaces their name, picture and detail if there is. Call it when someone signs up, and again whenever their profile changes in your product. Keep the `id` it returns beside your user.

A new name shows everywhere at once, in every document and open editor, because documents name people by Proof id. Seals already made keep the name they were sealed with.

| Parameter | Type | Description |
| --- | --- | --- |
| `external_id` | string, path, required | Your id for this user. URL-encode it. |
| `name` | string, required | Up to 48 characters, without control or invisible characters. |
| `avatar_url` | string | An `https` URL of a PNG, JPEG or WebP picture of at most 1 MB, on a public address. Proof fetches it once and keeps its own copy; send the same URL again and it isn't fetched again. `null` removes the picture. Leave it out to keep the current one. |
| `detail` | string | Up to 128 characters. `null` removes it. Leave it out to keep the current one. |

<Tabs syncKey="lang">
<TabItem label="curl">

```bash
curl https://api.proof.exalto.ai/v1/users/external/u_42 \
  -X PUT \
  -H "Authorization: Bearer $PROOF_SECRET_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Ada Lovelace",
    "avatar_url": "https://acme.com/avatars/42.png",
    "detail": "ada@acme.com"
  }'
```

</TabItem>
<TabItem label="Node">

```ts
const user = await proof.users.upsert("u_42", {
  name: "Ada Lovelace",
  avatar_url: "https://acme.com/avatars/42.png",
  detail: "ada@acme.com",
});
await db.users.update("u_42", { proof_user_id: user.id });
```

</TabItem>
</Tabs>

Returns the user: `201` when it was created, `200` when it was updated.

```json
{
  "id": "usr_5Kq2Lm8R",
  "external_id": "u_42",
  "name": "Ada Lovelace",
  "avatar_url": "https://api.proof.exalto.ai/v1/avatars/av_9Rk3Tq",
  "detail": "ada@acme.com",
  "created_at": "2026-10-12T09:14:03Z"
}
```

| Error | When |
| --- | --- |
| `400 invalid_parameter` on `name` | The name is empty, longer than 48 characters, or has control or invisible characters. |
| `400 invalid_parameter` on `external_id` | Empty, or longer than 128 characters. |
| `400 avatar_unreadable` | Proof couldn't fetch the picture, or it isn't a PNG, JPEG or WebP of at most 1 MB. |

## Retrieve a user

`GET /v1/users/{id}`

| Parameter | Type | Description |
| --- | --- | --- |
| `id` | string, path, required | The user's Proof id. |

<Tabs syncKey="lang">
<TabItem label="curl">

```bash
curl https://api.proof.exalto.ai/v1/users/usr_5Kq2Lm8R \
  -H "Authorization: Bearer $PROOF_SECRET_KEY"
```

</TabItem>
<TabItem label="Node">

```ts
const user = await proof.users.retrieve("usr_5Kq2Lm8R");
```

</TabItem>
</Tabs>

Returns the [user](#the-user-object), or `404 user_not_found`.

## Retrieve a user by your id

`GET /v1/users/external/{external_id}`

For when you have only your own id, for example before you've stored the Proof id.

| Parameter | Type | Description |
| --- | --- | --- |
| `external_id` | string, path, required | Your id for this user. URL-encode it. |

<Tabs syncKey="lang">
<TabItem label="curl">

```bash
curl https://api.proof.exalto.ai/v1/users/external/u_42 \
  -H "Authorization: Bearer $PROOF_SECRET_KEY"
```

</TabItem>
<TabItem label="Node">

```ts
const user = await proof.users.retrieveByExternalId("u_42");
```

</TabItem>
</Tabs>

Returns the [user](#the-user-object), or `404 user_not_found`.

## Update a user

`PATCH /v1/users/{id}`

Changes any of a user's fields by Proof id, including `external_id`: use it when your ids change, in a migration or when someone switches login method. Documents, seals and agents follow the Proof id, so nothing else moves.

| Parameter | Type | Description |
| --- | --- | --- |
| `id` | string, path, required | The user's Proof id. |
| `external_id` | string | A new id of yours for this user. It must not belong to another user. |
| `name` | string | As in [Create or update a user](#create-or-update-a-user). |
| `avatar_url` | string or null | As above. |
| `detail` | string or null | As above. |

<Tabs syncKey="lang">
<TabItem label="curl">

```bash
curl https://api.proof.exalto.ai/v1/users/usr_5Kq2Lm8R \
  -X PATCH \
  -H "Authorization: Bearer $PROOF_SECRET_KEY" \
  -H "Content-Type: application/json" \
  -d '{"external_id": "2f6c1e0a-7b1d-4c55-9e0f-3a8d2b6c9e41"}'
```

</TabItem>
<TabItem label="Node">

```ts
await proof.users.update("usr_5Kq2Lm8R", { external_id: "2f6c1e0a-7b1d-4c55-9e0f-3a8d2b6c9e41" });
```

</TabItem>
</Tabs>

Returns the [user](#the-user-object).

| Error | When |
| --- | --- |
| `409 external_id_taken` | Another user already has that `external_id`. Merge the two first if they're the same person. |
| `404 user_not_found` | No user with that id. |

## Delete a user

`DELETE /v1/users/{id}`

Removes the user from Proof. Call it when someone deletes their account in your product. Their `external_id` is free again afterwards; using it again creates a new person.

- Documents they wrote in keep the last name they had, without their picture, so the record of who wrote what stays readable.
- Documents they owned pass to their other members: those with the highest role become owners. A document with no other member passes to someone else who wrote in it, if anyone did, and is otherwise deleted, with its seals. One in the trash with no owner left is deleted.
- Their agents are revoked, and their open editors close.
- Their security keys and check-in history are deleted.

| Parameter | Type | Description |
| --- | --- | --- |
| `id` | string, path, required | The user's Proof id. |

<Tabs syncKey="lang">
<TabItem label="curl">

```bash
curl https://api.proof.exalto.ai/v1/users/usr_5Kq2Lm8R \
  -X DELETE \
  -H "Authorization: Bearer $PROOF_SECRET_KEY"
```

</TabItem>
<TabItem label="Node">

```ts
await proof.users.delete("usr_5Kq2Lm8R");
```

</TabItem>
</Tabs>

```json
{
  "id": "usr_5Kq2Lm8R",
  "deleted": true
}
```

:::caution
A document that still has other members isn't deleted when its owner is. Make someone an owner first with [`PUT /v1/documents/{document_id}/members/{user_id}`](/reference/api/members/#add-or-change-a-member) if you want a particular person to own it; otherwise every member with the highest role becomes an owner.
:::

## Merge users

`POST /v1/users/{id}/merge`

Moves everything from one user to another, then deletes the first. Use it when someone who started without an account signs up or signs in, so their work comes with them. See [Anonymous users](/guides/anonymous-users/).

What moves:

- their documents and memberships (where both users were members, the higher role is kept), and documents in their own trash
- the credit for everything they and their agents wrote, typed, shaped or accepted, in every document
- their security keys (after `into`'s own), their check-ins, their presence signals, and their check-in interval unless `into` already has one
- their agents. An agent for a client `into` already has an agent for joins that one, keeping the higher role in each document and its tokens, so it needn't be approved again.

Open editors of the user merged from lose their sessions: start new ones as `into`.

| Parameter | Type | Description |
| --- | --- | --- |
| `id` | string, path, required | The user to merge from. It's deleted afterwards. |
| `into` | string, required | The Proof id of the user to merge into. It must exist. |

<Tabs syncKey="lang">
<TabItem label="curl">

```bash
curl https://api.proof.exalto.ai/v1/users/usr_2Hd7Np4Q/merge \
  -H "Authorization: Bearer $PROOF_SECRET_KEY" \
  -H "Content-Type: application/json" \
  -d '{"into": "usr_5Kq2Lm8R"}'
```

</TabItem>
<TabItem label="Node">

```ts
const user = await proof.users.merge("usr_2Hd7Np4Q", { into: "usr_5Kq2Lm8R" });
```

</TabItem>
</Tabs>

Returns the user merged into.

```json
{
  "id": "usr_5Kq2Lm8R",
  "external_id": "u_42",
  "name": "Ada Lovelace",
  "avatar_url": null,
  "detail": "ada@acme.com",
  "created_at": "2026-10-30T18:20:00Z"
}
```

| Error | When |
| --- | --- |
| `400 invalid_parameter` on `into` | `into` is the same user. |
| `404 user_not_found` | Either user doesn't exist. |

## List a user's documents

`GET /v1/users/{id}/documents`

The documents a user is a member of, most recently edited or shared with them first, leaving out those in the trash. Use it to build your document list; keep your own folders and pins beside it, keyed by document id.

| Parameter | Type | Description |
| --- | --- | --- |
| `id` | string, path, required | The user's Proof id. |
| `ids` | string | Comma-separated document ids: only those of them the user may open now, as a member or by link access. For titles and access of documents you keep elsewhere, such as in a notification list. |
| `trashed` | boolean | `true` lists what's in their trash instead, most recently trashed first: documents an owner trashed for everyone, where they're an owner, and documents they put in their own. Default `false`. |
| `limit` | integer | 1 to 100. Default 50. |
| `after` | string | The `next` cursor from the previous page. |

<Tabs syncKey="lang">
<TabItem label="curl">

```bash
curl "https://api.proof.exalto.ai/v1/users/usr_5Kq2Lm8R/documents?limit=2" \
  -H "Authorization: Bearer $PROOF_SECRET_KEY"
```

</TabItem>
<TabItem label="Node">

```ts
const page = await proof.users.listDocuments("usr_5Kq2Lm8R", { limit: 2 });
```

</TabItem>
</Tabs>

Each item is a [document](/reference/api/documents/#the-document-object) with two more fields: `role`, this user's role in it (the higher of their member role and the link's), and `added_at`, when they became a member, or `null` for one they reach by link. With `trashed=true`, each has instead `everyone`, `true` when it's in the trash for everyone and `false` when only in theirs, and `in_trash_since`.

```json
{
  "data": [
    {
      "id": "doc_8Hq2Lm4R",
      "name": "On the Analytical Engine",
      "title": "On the Analytical Engine",
      "owners": ["usr_5Kq2Lm8R"],
      "link_access": "restricted",
      "created_at": "2026-10-13T21:12:40Z",
      "edited_at": "2026-10-13T23:30:52Z",
      "trashed_at": null,
      "role": "owner",
      "added_at": "2026-10-13T21:12:40Z"
    },
    {
      "id": "doc_3Wp7Zc1V",
      "name": null,
      "title": "Notes for Thursday",
      "owners": ["usr_9Tb3Wc1X"],
      "link_access": "suggest",
      "created_at": "2026-10-11T15:02:11Z",
      "edited_at": "2026-10-12T08:47:30Z",
      "trashed_at": null,
      "role": "suggester",
      "added_at": "2026-10-11T15:40:02Z"
    }
  ],
  "next": "c_2Fh8Kq"
}
```

## Empty a user's trash

`DELETE /v1/users/{id}/trash`

`DELETE /v1/users/{id}/trash/{document_id}`

Deletes for good everything in a user's trash, or one document there. A document in the trash for everyone, where they're an owner, is [deleted](/reference/api/documents/#delete-a-document) for everyone; one in their own trash they leave, as a member, and it stays for the others.

| Parameter | Type | Description |
| --- | --- | --- |
| `id` | string, path, required | The user's Proof id. |
| `document_id` | string, path | One document in their trash. |

<Tabs syncKey="lang">
<TabItem label="curl">

```bash
curl https://api.proof.exalto.ai/v1/users/usr_5Kq2Lm8R/trash \
  -X DELETE \
  -H "Authorization: Bearer $PROOF_SECRET_KEY"
```

</TabItem>
<TabItem label="Node">

```ts
await proof.users.emptyTrash("usr_5Kq2Lm8R");
await proof.users.deleteFromTrash("usr_5Kq2Lm8R", "doc_8Hq2Lm4R");
```

</TabItem>
</Tabs>

```json
{ "user": "usr_5Kq2Lm8R", "deleted": 3 }
```

For one document, `{ "user": …, "document": …, "deleted": true }`, or `404 document_not_found` when it isn't in their trash.

## End a user's sessions

`DELETE /v1/users/{id}/sessions`

Ends a user's editor and presence sessions, all of them or one `group`'s, as when they sign out. Described with the other session endpoints: see [End a user's sessions](/reference/api/sessions/#end-a-users-sessions).

## List a user's agents

`GET /v1/users/{id}/agents`

The agents a user has connected, newest first. This endpoint is described with the other agent endpoints: see [List a user's agents](/reference/api/agents/#list-a-users-agents).

## Retrieve check-in settings

`GET /v1/users/{id}/checkins`

How often a user is asked to check in with a security key while they write, and how many keys they have. See [Presence and check-ins](/concepts/presence-and-check-ins/).

| Parameter | Type | Description |
| --- | --- | --- |
| `id` | string, path, required | The user's Proof id. |

<Tabs syncKey="lang">
<TabItem label="curl">

```bash
curl https://api.proof.exalto.ai/v1/users/usr_5Kq2Lm8R/checkins \
  -H "Authorization: Bearer $PROOF_SECRET_KEY"
```

</TabItem>
<TabItem label="Node">

```ts
const checkins = await proof.users.retrieveCheckins("usr_5Kq2Lm8R");
```

</TabItem>
</Tabs>

```json
{
  "user": "usr_5Kq2Lm8R",
  "every_minutes": 10,
  "security_keys": 2
}
```

| Attribute | Type | Description |
| --- | --- | --- |
| `user` | string | The user's Proof id. |
| `every_minutes` | integer or null | `1`, `5`, `10` or `30`: about how often they're asked while writing. `null` when check-ins are off. |
| `security_keys` | integer | How many security keys they've added. Keys are added only in presence settings, on Proof's own domain. |

## Update check-in settings

`PUT /v1/users/{id}/checkins`

Turns check-ins on or off, or changes how often they're asked. People can also change this themselves in the [presence settings window](/guides/set-up-check-ins/), where they add their keys.

| Parameter | Type | Description |
| --- | --- | --- |
| `id` | string, path, required | The user's Proof id. |
| `every_minutes` | integer or null, required | `1`, `5`, `10` or `30`, or `null` to turn check-ins off. |

<Tabs syncKey="lang">
<TabItem label="curl">

```bash
curl https://api.proof.exalto.ai/v1/users/usr_5Kq2Lm8R/checkins \
  -X PUT \
  -H "Authorization: Bearer $PROOF_SECRET_KEY" \
  -H "Content-Type: application/json" \
  -d '{"every_minutes": 5}'
```

</TabItem>
<TabItem label="Node">

```ts
await proof.users.updateCheckins("usr_5Kq2Lm8R", { every_minutes: 5 });
```

</TabItem>
</Tabs>

```json
{
  "user": "usr_5Kq2Lm8R",
  "every_minutes": 5,
  "security_keys": 2
}
```

| Error | When |
| --- | --- |
| `400 invalid_parameter` on `every_minutes` | Not one of `1`, `5`, `10`, `30` or `null`. |
| `409 no_security_keys` | The user has no security keys yet. Open presence settings for them to add one. |