Canonical page: https://docs.singlebase.io/users

# Users

Manage your project's users from a trusted server or AI agent with `users.*` operations.

## Overview

**Users** is the admin side of Identity: the API for your project's user records, the same accounts that sign in through [Authentication](/auth.md). Call it from your backend, or let a support or ops agent manage users within the permissions you grant.

**Key features:**
- Create users, with or without a password
- Look up users by ID, email, username, or linked provider
- Search and paginate users
- Update profiles and application data
- Suspend users, with scheduled status transitions
- Soft-delete or permanently purge users
- Link and unlink OAuth provider identities

**Use cases:** Admin dashboards, back-office tools, imports, support agents, moderation

**Access:** Admin only. Call `users.*` with a server key (`sk_`). An [agent key](/agents.md) can call permitted user operations, including `users.get_details` with `users.list`; agent keys can never call `users.get`. Don't send a bearer token: with one, the request acts as that user and is denied. Never call these operations from a browser.

::: tip Self-service vs admin
- A signed-in user reads and edits **their own** profile with [`user.get` and `user.update`](/user.md).
- Your server manages **any** user with `users.*`.
- Passwords, codes, and sessions belong to [`auth.*`](/auth.md).
:::

---

## Operations

| Operation | Description | Selector |
|---|---|---|
| `users.create` | Create a user | None |
| `users.get` | Get one user | One of `user_id`, `id`, `email`, `username`; or `provider` + `provider_user_id` |
| `users.get_details` | Get a user with providers, login methods, and recent events | One of `user_id`, `id`, `email`, `username` |
| `users.query` | Search and list users | None |
| `users.update` | Update profile and application data | `user_id` or `id` |
| `users.update_status` | Change status, optionally on a schedule | `user_id` or `id` |
| `users.update_password` | Set a new password | `user_id` or `id` |
| `users.delete` | Soft-delete or purge a user | `user_id` or `id` |
| `users.link_provider` | Link a provider identity | `user_id` or `id` |
| `users.unlink_provider` | Unlink a provider identity | Provider identity |

`id` is an alias for `user_id` in selectors. Send one or the other, not both.

---

## User Fields

| Field | Description |
|---|---|
| `id` | User ID |
| `email`, `username`, `phone` | Identity fields |
| `first_name`, `last_name`, `profile_photo` | Profile fields |
| `status` | `active` by default. Other values are up to your app, such as `suspended` or `banned` |
| `requires_password_change` | When `true`, the user must reset their password before signing in |
| `email_verified_at`, `phone_verified_at` | Verification times |
| `data` | Application data: `timezone`, `locale`, `metadata`, and `claims` |

**Roles** live in `data.claims`, keyed by audience. For example, `{"web": ["user"]}` gives the role `user` in the `web` audience.

Only `active` users that are not deleted can sign in.

---

## users.create

```json
{
  "operation": "users.create",
  "payload": {
    "email": "ada@example.com",
    "password": "ExamplePass123!",
    "first_name": "Ada",
    "last_name": "Lovelace",
    "data": {
      "timezone": "Europe/London",
      "locale": "en-GB",
      "metadata": { "plan": "pro" },
      "claims": { "web": ["user", "editor"] }
    }
  }
}
```

**Parameters (all optional):**

| Field | Description |
|---|---|
| `user_id` | Your own ID. A UUID is generated when omitted |
| `email`, `username`, `phone` | Identity fields |
| `first_name`, `last_name`, `profile_photo` | Profile fields |
| `password` | Plain password. It is checked against the password policy and stored hashed |
| `status`, `status_reason` | Initial status. Default `active` |
| `requires_password_change` | Force a password reset on first signin. Default `false` |
| `provider`, `provider_user_id` | A provider identity your server has already verified |
| `data` | Application data. `claims` defaults to `{"web": ["user"]}` |

**Behavior:**
- No welcome email is sent and no session is created. Use [`auth.signup`](/auth.md#auth-signup) for the normal signup flow.
- `password_hash` and `password_algo` are rejected. Send `password`.
- Put roles in `data.claims`, not `data.auth.claims`.

---

## users.get

```json
{
  "operation": "users.get",
  "payload": { "email": "ada@example.com" }
}
```

**Example - By provider identity:**
```json
{
  "operation": "users.get",
  "payload": { "provider": "github", "provider_user_id": "827364" }
}
```

**Response:**
```json
{
  "data": {
    "item": {
      "id": "user_123",
      "email": "ada@example.com",
      "first_name": "Ada",
      "last_name": "Lovelace",
      "status": "active",
      "data": { "claims": { "web": ["user"] } }
    }
  }
}
```

The record is returned under `data.item`. Admin responses are not filtered like [`user.get`](/user.md#user-get); don't forward them to public clients.

---

## users.get_details

Get a user with linked providers, login methods, and recent identity events.

```json
{
  "operation": "users.get_details",
  "payload": { "user_id": "user_123" }
}
```

Accepts one of `user_id`, `id`, `email`, or `username`. To look up by provider, call `users.get` first.

---

## users.query

```json
{
  "operation": "users.query",
  "payload": {
    "status": "active",
    "search": "ada",
    "filter": { "data.metadata.plan:$in": ["pro", "enterprise"] },
    "page": 1,
    "per_page": 25
  }
}
```

**Parameters (all optional):**

| Field | Description |
|---|---|
| `search` | Matches ID, email, username, or phone. Alias `q` |
| `status`, `email`, `username` | Exact matches |
| `filter` | [Filter](/queries.md) on application data. Paths must start with `data.` |
| `page`, `per_page` | Page-based pagination |
| `limit`, `offset` | Offset-based pagination |

Returns users in `data` and pagination in `meta.pagination`. Use `status`, `email`, and `username` instead of filtering those fields.

---

## users.update

```json
{
  "operation": "users.update",
  "payload": {
    "user_id": "user_123",
    "first_name": "Ada",
    "data": { "timezone": "Europe/London" }
  }
}
```

**Updatable fields:** `email`, `username`, `phone`, `first_name`, `last_name`, `profile_photo`, `requires_password_change`, `email_verified_at`, `phone_verified_at`, `data`

**Behavior:**
- `data` is merged deeply; nested fields you omit are kept.
- To replace `data` entirely, send `"data": {"$replace": {...}}`. This can remove `claims`, so use it with care.
- Status and passwords have their own operations.

---

## users.update_status

Suspend, ban, or reactivate a user. Optionally schedule the next status.

```json
{
  "operation": "users.update_status",
  "payload": {
    "user_id": "user_123",
    "status": "suspended",
    "status_reason": "Payment overdue",
    "status_expires_in": 172800,
    "status_next": "active",
    "status_next_reason": "Suspension ended",
    "changed_by": "admin:42"
  }
}
```

**Parameters:**

| Field | Description |
|---|---|
| `status` | New status (required) |
| `status_reason` | Why it changed |
| `status_expires_in` | Seconds until `status_next` applies |
| `status_expires_at` | Time when `status_next` applies. Use one expiration field only |
| `status_next` | Status to apply when the current one expires. Required with an expiration |
| `status_next_reason` | Reason for the scheduled change |
| `changed_by` | Who made the change, for the audit trail |

A non-`active` status blocks new signins. Session verification also checks that the user is active, so suspended or deleted users cannot continue authenticated requests. Status updates do not themselves consume every stored session token.

---

## users.update_password

```json
{
  "operation": "users.update_password",
  "payload": {
    "user_id": "user_123",
    "password": "NewExamplePass123!"
  }
}
```

The password is checked against the policy and stored hashed. Existing sessions are revoked. Returns `data: {"id": "user_123", "updated": true}`.

---

## users.delete

```json
{
  "operation": "users.delete",
  "payload": {
    "user_id": "user_123",
    "status_reason": "User requested deletion"
  }
}
```

**Soft delete (default):** Sets `status` to `deleted`, revokes sessions, and keeps the email and provider identities reserved.

**Purge:** Add `"purge": true` to permanently remove the user, providers, tokens, and identity events.

::: danger
Purging cannot be undone. It does not delete the user's documents or files. Remove those separately with [`data.delete`](/data.md#data-delete) and [`files.delete`](/files.md#files-delete).
:::

---

## Provider Identities

### users.link_provider

```json
{
  "operation": "users.link_provider",
  "payload": {
    "user_id": "user_123",
    "provider": "github",
    "provider_user_id": "827364",
    "email": "ada@example.com"
  }
}
```

Requires a user selector, `provider`, and `provider_user_id`. Optional: `email`, `data`. Only link identities your server has verified.

### users.unlink_provider

```json
{
  "operation": "users.unlink_provider",
  "payload": {
    "provider": "github",
    "provider_user_id": "827364"
  }
}
```

Requires `provider` and `provider_user_id`. `user_id` or `id` is optional.

---

## Common Errors

| Message | Meaning |
|---|---|
| `AUTHENTICATION_REQUIRED`, `ACCESS_DENIED` | The caller is not a server or agent key, or the request included a user's bearer token |
| `AGENT_SCOPE_DENIED` | The agent key lacks the needed `users.*` permission |
| `INVALID_PAYLOAD` | Missing or conflicting selectors, or an invalid status schedule |
| `MISSING_USER_SELECTOR` | No user selector was sent |

See [Responses](/api.md#responses) for the error envelope.

---

## Next Steps

- [Authentication](/auth.md) - Signup, signin, and sessions for end users
- [Agents](/agents.md) - Let an AI agent manage users with scoped permissions
- [Data](/data.md) - Query documents owned by a user
