Skip to content

Users

Manage operators and their permissions. See the Permissions guide for available permissions and templates.

const TOKEN = "hv_..."; // your API key
const headers = { Authorization: `Bearer ${TOKEN}` };
POST /admin/users

Requires users:create permission. You cannot grant permissions you don’t have yourself (escalation guard).

interface User {
id: string;
did: string;
is_super: boolean;
permissions: string[];
}
interface CreatedUser {
id: string;
// The resolved DID, even when a handle was sent.
did: string;
}
const response = await fetch("http://127.0.0.1:3000/admin/users", {
method: "POST",
headers: {
...headers,
"Content-Type": "application/json",
},
body: JSON.stringify({
did: "did:plc:newuser",
template: "maintainer",
}),
});
const data: CreatedUser = await response.json();
FieldTypeRequiredDescription
didstringyesThe DID or handle of the operator to add. A leading @ is fine. A handle is resolved to a DID
templatestringnoPermission template: viewer, maintainer, manager, or full_access. operator, the old name for maintainer, is still accepted
permissionsstring[]noExplicit list of permissions to grant. When present, template is ignored

If neither template nor permissions is provided, the operator gets the viewer template.

Returns 400 Bad Request if a handle can’t be resolved, 403 Forbidden if you grant a permission you don’t hold, and 409 Conflict if the operator already exists.

Response: 201 Created

{
"id": "550e8400-e29b-41d4-a716-446655440000",
"did": "did:plc:newuser"
}
GET /admin/users

Requires users:read permission. Operators are ordered by created_at, and each permissions array is sorted alphabetically.

interface UserWithTimestamps {
id: string;
did: string;
is_super: boolean;
permissions: string[];
created_at: string;
last_used_at: string | null;
}
const response = await fetch("http://127.0.0.1:3000/admin/users", {
headers,
});
const data: UserWithTimestamps[] = await response.json();

Response: 200 OK

[
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"did": "did:plc:admin",
"is_super": true,
"permissions": ["api-keys:create", "api-keys:delete", "api-keys:read", "backfill:create", "backfill:read", "events:read", "lexicons:create", "lexicons:delete", "lexicons:read", "records:delete", "records:delete-collection", "records:read", "script-variables:create", "script-variables:delete", "script-variables:read", "stats:read", "users:create", "users:delete", "users:read", "users:update"],
"created_at": "2025-01-01T00:00:00Z",
"last_used_at": "2025-01-02T12:00:00Z"
}
]
GET /admin/users/{id}

Requires users:read permission.

const response = await fetch(
"http://127.0.0.1:3000/admin/users/550e8400-e29b-41d4-a716-446655440000",
{ headers },
);
const data: UserWithTimestamps = await response.json();

Response: 200 OK with the same shape as a single item from the list response.

PATCH /admin/users/{id}/permissions

Requires users:update permission. You cannot grant permissions you don’t have yourself, and you cannot modify the permissions of an operator with superadmin.

const response = await fetch(
"http://127.0.0.1:3000/admin/users/550e8400-e29b-41d4-a716-446655440000/permissions",
{
method: "PATCH",
headers: {
...headers,
"Content-Type": "application/json",
},
body: JSON.stringify({
grant: ["lexicons:create", "lexicons:delete"],
revoke: ["records:delete"],
}),
},
);
if (!response.ok) throw new Error(`update failed: ${response.status}`); // 204 No Content
FieldTypeRequiredDescription
grantstring[]noPermissions to add
revokestring[]noPermissions to remove

Response: 204 No Content. Fetch the user again to see its permissions.

POST /admin/users/transfer-super

Only an operator with superadmin can call this endpoint. It gives superadmin to another existing operator and takes it from the caller.

const response = await fetch("http://127.0.0.1:3000/admin/users/transfer-super", {
method: "POST",
headers: {
...headers,
"Content-Type": "application/json",
},
body: JSON.stringify({
target_user_id: "550e8400-e29b-41d4-a716-446655440000",
}),
});
FieldTypeRequiredDescription
target_user_idstringyesThe ID of the operator to receive superadmin

Response: 204 No Content

DELETE /admin/users/{id}

Requires users:delete permission. You cannot delete an operator with superadmin, or yourself.

const response = await fetch(
"http://127.0.0.1:3000/admin/users/550e8400-e29b-41d4-a716-446655440000",
{
method: "DELETE",
headers,
},
);

Response: 204 No Content