Users API
Provision Programmatic Users — login-disabled service accounts — and mint API tokens for them, entirely from an API token. This is the token-driven counterpart to the dashboard's Configure → Members page and its per-user API Tokens tab.
All endpoints live under /api/client/v1 and are authenticated with a cpeer_ API token:
Authorization: Bearer cpeer_…Both endpoints require an owner or admin token (the same gate as the Members dashboard page); any other token gets 403 Forbidden.
Programmatic Users only
POST /client/v1/users can never create a login-capable account. canLogin is forced to false server-side regardless of what the request body sends — there is no field that overrides this. To provision a human teammate with a password, use the dashboard's Invite flow (Configure → Members) or the session-authenticated members API instead.
Create a Programmatic User
POST /api/client/v1/users
Authorization: Bearer cpeer_…
Content-Type: application/json{
"name": "billing-sync-bot",
"email": "billing-sync-bot@example.com",
"role": "user",
"servicePermissions": {
"models": "read",
"vector": "write"
}
}| Field | Type | Required | Notes |
|---|---|---|---|
name | string | yes | Trimmed; must be non-empty. |
email | string | no | Programmatic Users don't need one — omit it entirely. When supplied it must be a valid address and unique within the tenant. |
role | user | project_admin | admin | no | Defaults to user. |
servicePermissions | object | no | Per-service permission overrides (none | read | write | admin), same shape as a member's service permissions. Normalized server-side. |
canLogin is not an accepted input field — it is always set to false on users created through this endpoint.
Response
201 Created
{
"user": {
"_id": "66aa1f2e9c1d4400123abcde",
"canLogin": false,
"createdAt": "2026-05-01T09:00:00.000Z",
"email": "billing-sync-bot@example.com",
"invitedAt": "2026-05-01T09:00:00.000Z",
"invitedBy": "6650…",
"name": "billing-sync-bot",
"role": "user",
"servicePermissions": { "models": "read", "vector": "write" },
"updatedAt": "2026-05-01T09:00:00.000Z"
}
}invitedBy is the user id of the token owner that made the call. The user has a randomly generated password hash under the hood (unusable, since canLogin is false) — no password or temp-password field is ever returned.
Mint a token for a user
POST /api/client/v1/users/:id/tokens
Authorization: Bearer cpeer_…
Content-Type: application/jsonMints a new API token owned by the target user (:id) and returns the plaintext secret once — it is never retrievable again after this response. Works for any user in the caller's tenant, not just Programmatic Users: this is how you mint a service token for a login-disabled account (which can never reach the session-based token page itself), but it can also mint an extra token for a regular member.
{
"label": "billing-sync nightly job",
"servicePermissions": {
"models": "read",
"vector": "write"
}
}| Field | Type | Required | Notes |
|---|---|---|---|
label | string | yes | At least 3 characters. |
servicePermissions | object | no | Per-service scope for the minted token. Omit for an unscoped token that inherits the target user's own effective permissions. |
Scope clamping: the requested (or inherited) scope is always clamped down to the target user's own effective permissions, and additionally clamped to the calling token's own scope whenever the caller is itself a scoped token — a scoped token can never mint a token that reaches further than itself, for any target user, including one it just created via the endpoint above.
Response
201 Created
{
"id": "665f1a2b3c4d5e6f70819a2b",
"label": "billing-sync nightly job",
"message": "API token created successfully",
"servicePermissions": { "models": "read", "vector": "write" },
"token": "cpeer_9f2a1c...redacted",
"userId": "66aa1f2e9c1d4400123abcde"
}token is the only time the plaintext secret is returned. servicePermissions is null on an unscoped token.
Errors
| Status | Cause |
|---|---|
| 400 | name missing/blank, invalid role, invalid email format, or label shorter than 3 characters. |
| 401 | Missing or invalid API token. |
| 403 | Caller's token is not owned by an owner/admin user. |
| 404 | :id does not resolve to a user in the caller's tenant. |
| 409 | email already belongs to another user in the organization. |
| 429 | User quota exceeded (create) or API token quota exceeded (mint). |
| 500 | Internal error. |
Example
Create a Programmatic User, then mint a scoped token for it:
# Create a login-disabled service account
curl -X POST https://your-console.example.com/api/client/v1/users \
-H "Authorization: Bearer cpeer_…" \
-H "Content-Type: application/json" \
-d '{
"name": "billing-sync-bot",
"role": "user",
"servicePermissions": { "models": "read", "vector": "write" }
}'
# Mint an API token for the user just created (id from the response above)
curl -X POST https://your-console.example.com/api/client/v1/users/66aa1f2e9c1d4400123abcde/tokens \
-H "Authorization: Bearer cpeer_…" \
-H "Content-Type: application/json" \
-d '{
"label": "billing-sync nightly job",
"servicePermissions": { "models": "read", "vector": "write" }
}'Store the returned token immediately — it is shown once, both here and in the dashboard's per-user token page.

