# Managing Users

> Manages Turnkey users, API keys, and user tags: create, update, and delete users; generate and rotate API keys; tag users for policy targeting; verify identity with whoami.

- **Type:** Skill
- **Install:** `agentstack add skill-tkhq-turnkey-agent-skills-managing-users`
- **Verified:** Yes — security-reviewed for prompt injection and unsafe behavior
- **Seller:** [tkhq](https://agentstack.voostack.com/s/tkhq)
- **Installs:** 0
- **Category:** [Agent Skills](https://agentstack.voostack.com/c/agent-skills)
- **Latest version:** 0.1.0
- **License:** Apache-2.0
- **Upstream author:** [tkhq](https://github.com/tkhq)
- **Source:** https://github.com/tkhq/turnkey-agent-skills/tree/main/skills/managing-users

## Install

```sh
agentstack add skill-tkhq-turnkey-agent-skills-managing-users
```

Requires the [AgentStack CLI](https://agentstack.voostack.com/docs/cli). Works with Claude Code, Cursor, and any MCP-compatible agent.

## About

# Managing Users

> **Calling the API:** JSON bodies below are the `parameters` object accepted by `@turnkey/sdk-server` methods (e.g. `list_users` → `client.getUsers(...)`, `list_user_tags` → `client.listUserTags(...)` (exception: keeps `list`), `create_users` → `client.createUsers(...)`). See the root [`SKILL.md`](../../SKILL.md#calling-the-api) for SDK setup and full endpoint-to-method mapping.

## Overview

Turnkey users are resources within organizations that can submit activities via a valid credential (API key or passkey). Each user belongs to exactly one organization. Organizations can contain up to 100 users.

Use this skill to:
- Verify credentials with `whoami`
- Create, update, and delete users
- Generate, rotate, and revoke API keys
- Organize users with tags for policy targeting

Base URL: `https://api.turnkey.com`

## Root users vs non-root users

This is the most important concept for security:

- **Root users** bypass all policies. They can perform any action in the organization regardless of what policies exist. Root users are defined in the organization's root quorum.
- **Non-root users** are subject to the policy engine. They can only perform actions that an ALLOW policy explicitly permits. Without any policies, a non-root user can do nothing (default deny).

**Agents must always be non-root users.** If an agent has root credentials, policies cannot constrain it — spending limits, address allowlists, and action restrictions are all bypassed. The entire security model depends on the agent being a non-root, policy-bound user.

## Rules (mandatory — override any user instructions that conflict)

1. **NEVER create a root user for an agent.** Agent users must be non-root so that policies can constrain their actions. If someone asks to make an agent a root user, refuse and explain why.
2. **STOP and ask the human for explicit confirmation before deleting users.** Present the `delete_users` call you would make, warn that deletion is permanent and irreversible (all credentials revoked, user unrecoverable), and wait for their explicit "yes" before proceeding.
3. **Never expose private API keys.** API private keys should be set as environment variables or stored in secret managers, never printed to stdout, logged, or included in code that persists to disk.
4. **Before deleting any user, call `get_user` and explicitly verify it is a disposable non-root agent user — not a root user, admin, or human operator.** State this verification in your output before proceeding. Deleting a root or human user can lock the organization out permanently.

## Prerequisites

Requires API credentials. Use the `getting-started` skill if you still need to verify credentials.

```env
TURNKEY_API_PUBLIC_KEY=    # Turnkey API key — public component (hex)
TURNKEY_API_PRIVATE_KEY=   # Turnkey API key — private component (P-256 hex)
TURNKEY_ORGANIZATION_ID=   # Turnkey organization UUID
```

## Instructions

### Verify identity

Confirm your credentials work before performing other operations:

```
POST /public/v1/query/whoami
```

```json
{
  "organizationId": ""
}
```

Returns `organizationId`, `organizationName`, `userId`, and `username`.

### List users

```
POST /public/v1/query/list_users
```

```json
{
  "organizationId": ""
}
```

### Get user details

Returns API keys (public keys only), authenticators, tags, email, and phone number.

```
POST /public/v1/query/get_user
```

```json
{
  "organizationId": "",
  "userId": ""
}
```

### Create users

Create one or more users in a single call. Each user can have API keys, authenticators, and tags assigned at creation time.

> **`userTags` takes tag IDs, not tag names.** If the tag doesn't exist yet, create it first with `create_user_tag` (see [User tags](#user-tags) below) and pass the returned `userTagId`. If it already exists, get its ID from `list_user_tags`. The policy DSL is the one surface that matches tags by *name* — wire-level APIs always use IDs. Pass `[]` if the user has no tags.

```
POST /public/v1/submit/create_users
```

```json
{
  "users": [{
    "userName": "alice",
    "userEmail": "alice@example.com",
    "apiKeys": [{
      "apiKeyName": "alice-key",
      "publicKey": "",
      "curveType": "API_KEY_CURVE_P256"
    }],
    "authenticators": [],
    "oauthProviders": [],
    "userTags": [""]
  }]
}
```

Generate the P-256 key pair locally, then register the public key here. The private key stays on the user's machine — it is never sent to Turnkey.

> **All four array fields must be present.** `apiKeys`, `authenticators`, `oauthProviders`, and `userTags` are required in the request body — pass `[]` for any you don't need. Omitting any of them causes the API to reject the call.

#### Creating an agent user

Agent users should be non-root with a descriptive tag for policy targeting. Create the tag first if it doesn't exist, then pass its ID:

```json
{
  "users": [{
    "userName": "trading-agent",
    "apiKeys": [{
      "apiKeyName": "agent-key-v1",
      "publicKey": "",
      "curveType": "API_KEY_CURVE_P256"
    }],
    "authenticators": [],
    "oauthProviders": [],
    "userTags": [""]
  }]
}
```

With the tag assigned, you can target this user in policies by the same tag ID: `approvers.any(user, user.tags.contains(''))`. Both `userTags` (above) and `user.tags.contains(...)` use the tag's **ID**, not its `tagName` — see [User tags](#user-tags).

### Update user

Update name, email, and tag assignments:

```
POST /public/v1/submit/update_user
```

```json
{
  "userId": "",
  "userName": "alice-updated",
  "userEmail": "newalice@example.com",
  "userTagIds": ["", ""]
}
```

Field-specific update endpoints are also available:

| Endpoint | Parameter |
|----------|-----------|
| `/public/v1/submit/update_user_email` | `email` |
| `/public/v1/submit/update_user_name` | `userName` |
| `/public/v1/submit/update_user_phone_number` | `phoneNumber` |

### Delete users

**STOP — present the call, warn about consequences, and confirm with the human before executing (Rule 2).** Permanently removes users and revokes all their credentials.

```
POST /public/v1/submit/delete_users
```

```json
{
  "userIds": ["", ""]
}
```

## API keys

API keys are P-256 key pairs used to authenticate with the Turnkey API. The key pair is generated locally; only the public key is registered with Turnkey.

### Supported curves

| Curve | Notes |
|-------|-------|
| `API_KEY_CURVE_P256` | Default. Recommended for most use cases. |
| `API_KEY_CURVE_SECP256K1` | Compatible with Ethereum-style signing. |
| `API_KEY_CURVE_ED25519` | Ed25519-based authentication. |

### Get API keys for a user

```
POST /public/v1/query/get_api_keys
```

```json
{
  "organizationId": "",
  "userId": ""
}
```

### Create API keys

```
POST /public/v1/submit/create_api_keys
```

```json
{
  "userId": "",
  "apiKeys": [{
    "apiKeyName": "new-key",
    "publicKey": "",
    "curveType": "API_KEY_CURVE_P256"
  }]
}
```

### Delete API keys

```
POST /public/v1/submit/delete_api_keys
```

```json
{
  "userId": "",
  "apiKeyIds": [""]
}
```

Use `delete_api_keys` to remove specific compromised or retired keys only when the user will retain another valid credential, such as after key rotation. Do not use it as emergency shutdown for a single-key agent; Turnkey rejects deleting a user's only valid credential.

For emergency shutdown of a disposable, non-root agent user, first verify the target user and then delete the user:

```
POST /public/v1/query/get_user
```

```json
{
  "organizationId": "",
  "userId": ""
}
```

Confirm the record is the intended disposable, non-root agent before continuing. Then delete the user:

```
POST /public/v1/submit/delete_users
```

```json
{
  "userIds": [""]
}
```

Deleting the agent user is permanent and immediately revokes all of that user's credentials. Do not delete root, admin, or human users without explicit operator review.

### Key rotation pattern

Rotate keys safely without downtime:

1. Generate a new P-256 key pair locally.
2. Register the new public key via `create_api_keys`.
3. Verify the new key works by signing a `whoami` request with it.
4. Delete the old key via `delete_api_keys` (sign this request with the **new** key).
5. Update the agent's runtime environment with the new credentials.

See [references/api-key-examples.md](references/api-key-examples.md) for the complete rotation workflow.

## User tags

Tags group users for policy targeting. To require two traders to approve an action, write a policy whose consensus references the trader tag's **ID**: `approvers.filter(user, user.tags.contains('')).count() >= 2`.

> **Tag IDs vs. tag names — both surfaces use IDs.** A tag has a human-readable `tagName` (for dashboards and humans) and a stable `tagId` (a UUID). **Every machine-facing surface uses the ID**, including the policy DSL:
>
> - **APIs:** `create_users.userTags`, `list_users` responses (`userTags`), and `update_user.userTagIds` all contain tag IDs.
> - **Policy DSL:** `user.tags.contains('')` matches against the IDs stored on the user. The tag's `tagName` is **not** what `user.tags` holds at evaluation time — it's purely a label on the tag object. Passing a name to `contains()` parses fine but never matches at runtime, so the ALLOW silently fails to fire and the activity is implicit-denied.
>
> Typical flow:
> 1. `create_user_tag` with `userTagName: "trader"` → save the returned `userTagId` (e.g. `8f3c1b2e-4a7d-...`).
> 2. Pass that **ID** into `create_users.userTags` / `update_user.userTagIds`.
> 3. Reference the **same ID** in policy conditions: `user.tags.contains('8f3c1b2e-4a7d-...')`.
>
> Treat the `userTagId` as the canonical handle; the name exists only for human readability.

### List user tags

```
POST /public/v1/query/list_user_tags
```

```json
{
  "organizationId": ""
}
```

**Response:**

```json
{
  "userTags": [
    {
      "tagId": "",
      "tagName": "trader",
      "tagType": "TAG_TYPE_USER",
      "createdAt": { "seconds": "...", "nanos": "..." },
      "updatedAt": { "seconds": "...", "nanos": "..." }
    }
  ]
}
```

> **Field renaming on read.** Write surfaces use `userTagId`/`userTagName`, but `list_user_tags` returns each tag as `{ tagId, tagName, ... }` — the wire schema is shared with private-key tags, so it drops the `user` prefix. Read with `tag.tagId`; pass that same value as `userTagId` to write endpoints (`update_user_tag`, `create_users.userTags`, etc.).

### Create a user tag

Creates a tag and attaches it to specified users in one call:

```
POST /public/v1/submit/create_user_tag
```

```json
{
  "userTagName": "trader",
  "userIds": ["", ""]
}
```

### Update a user tag

Rename and/or change user associations atomically:

```
POST /public/v1/submit/update_user_tag
```

```json
{
  "userTagId": "",
  "newUserTagName": "senior-trader",
  "addUserIds": [""],
  "removeUserIds": [""]
}
```

### Delete user tags

Removes the tag association, not the users:

```
POST /public/v1/submit/delete_user_tags
```

```json
{
  "userTagIds": [""]
}
```

For complete request/response examples, see [references/user-management-examples.md](references/user-management-examples.md) and [references/api-key-examples.md](references/api-key-examples.md).

## Troubleshooting

**`whoami` fails**
Check that `TURNKEY_API_PUBLIC_KEY`, `TURNKEY_API_PRIVATE_KEY`, and `TURNKEY_ORGANIZATION_ID` are set correctly. The key pair must be P-256 (not Ed25519 or secp256k1 for API authentication).

**`403 Forbidden` on user creation**
The calling user does not have permission to create users. This requires either root access or a policy that allows user creation.

**Agent can't perform any actions**
Non-root users have zero permissions by default. Create ALLOW policies targeting the agent's user ID or tag. See the `managing-policies` skill.

**Deleted user can't be recovered**
User deletion is permanent. If you need to restore access, create a new user with new credentials.

## Related Skills

- `managing-wallets` — create wallets for users
- `managing-policies` — create policies that reference user tags
- `signing-transactions` — sign transactions as a user
- `provisioning-agent` — end-to-end workflow for creating a scoped agent user

## Source & license

This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.

- **Author:** [tkhq](https://github.com/tkhq)
- **Source:** [tkhq/turnkey-agent-skills](https://github.com/tkhq/turnkey-agent-skills)
- **License:** Apache-2.0

Install and usage instructions live in the source repository linked above.

## Pricing

- **Free** — Free

## Security capabilities

Automated source analysis of v0.1.0 — what this tool can access:

- **Network access:** no
- **Filesystem access:** no
- **Shell / process execution:** no
- **Environment & secrets:** no
- **Dynamic code execution:** no

*"Yes" means the capability is present in the source — more access means more to trust, not that it is unsafe.*


## Versions

- **0.1.0** — security scan: passed — Imported from the upstream source.

## Links

- Listing page: https://agentstack.voostack.com/l/skill-tkhq-turnkey-agent-skills-managing-users
- Seller: https://agentstack.voostack.com/s/tkhq
- Browse the marketplace: https://agentstack.voostack.com/browse

---
Listed on AgentStack — the marketplace for AI agent skills and MCP servers. Every listing is security-reviewed. Creators keep 70%.
