Copy as markdown[View .md](https://docs.funnelfizz.com/ai-agents/admin-actions "View the raw markdown for this page")[Open in Claude](https://claude.ai/new?q=Read%20https%3A%2F%2Fdocs.funnelfizz.com%2Fai-agents%2Fadmin-actions.md%20and%20help%20me%20with%20this%20FunnelFizz%20topic%3A%20Admin%20actions "Open this page in Claude with context")[Open in ChatGPT](https://chat.openai.com/?q=Read%20https%3A%2F%2Fdocs.funnelfizz.com%2Fai-agents%2Fadmin-actions.md%20and%20help%20me%20with%20this%20FunnelFizz%20topic%3A%20Admin%20actions "Open this page in ChatGPT with context")

# Admin actions

Workspace administration — team management, billing, API keys, ownership transfer, workspace deletion — is gated by an out-of-band email code-paste handshake. This is **T2**, the highest safety tier. See [Safety tiers](https://docs.funnelfizz.com/ai-agents/safety-tiers.md#t2--admin-code-paste) for the model.

## Why it exists[​](#why-it-exists "Direct link to Why it exists")

Agents are good at typing, bad at judgment. A prompt-injected agent could be tricked into:

* Inviting an attacker to your workspace
* Revoking your only working API key
* Cancelling your subscription
* Deleting the workspace

The code-paste handshake makes these actions require *physical possession* of the API-key-holder's email inbox. Even a fully-compromised agent cannot fake a code that arrives via a different channel.

## The two-call flow[​](#the-two-call-flow "Direct link to The two-call flow")

```
┌──────────────────────────────────────────────────────────────────┐

│ STEP 1: agent calls admin.request_action                         │

│                                                                  │

│  Agent → MCP server:                                             │

│    admin.request_action({                                        │

│      action: "team.invite_member",                               │

│      subject: "alice@example.com",                               │

│      summary: "Invite alice@example.com as MANAGER"              │

│    })                                                            │

│                                                                  │

│  Server:                                                         │

│    - generates 6-digit code                                      │

│    - stores SHA-256(code) in OobConfirmCode                      │

│    - emails code to the API-key-holder user                      │

│    - returns { requestId, expiresAt, codeHint: "••••••" }        │

└──────────────────────────────────────────────────────────────────┘



┌──────────────────────────────────────────────────────────────────┐

│ STEP 2: user pastes the code; agent confirms                     │

│                                                                  │

│  Agent → User (in chat):                                         │

│    "Check your email for a 6-digit code from FunnelFizz."        │

│                                                                  │

│  User reads email, pastes code in chat.                          │

│                                                                  │

│  Agent → MCP server:                                             │

│    admin.confirm_action({ requestId, code: "482910" })           │

│                                                                  │

│  Server:                                                         │

│    - validates code (SHA-256 hash match)                         │

│    - increments attempts counter                                 │

│    - on match: mints AdminToken bound to (action, subject)       │

│    - returns { adminToken, expiresAt }                           │

└──────────────────────────────────────────────────────────────────┘



┌──────────────────────────────────────────────────────────────────┐

│ STEP 3: agent calls the destructive tool with admin_token        │

│                                                                  │

│  Agent → MCP server:                                             │

│    team.invite_member({                                          │

│      email: "alice@example.com",                                 │

│      role: "MANAGER",                                            │

│      adminToken: "ffa_..."                                       │

│    })                                                            │

│                                                                  │

│  Server:                                                         │

│    - consumes admin_token (single-use, atomic)                   │

│    - verifies action matches "team.invite_member"                │

│    - verifies subject matches "alice@example.com"                │

│    - sends the invite                                            │

└──────────────────────────────────────────────────────────────────┘
```

## Sample agent transcript[​](#sample-agent-transcript "Direct link to Sample agent transcript")

```
USER: Invite alice@example.com to the workspace as a manager.



AGENT: I'm requesting an admin confirmation code for that. Check your

       email for a 6-digit code from FunnelFizz — it'll arrive in a few

       seconds. The code expires in 10 minutes.



       (Agent calls admin.request_action.)



USER: Got it — 482910.



AGENT: Confirmed. Inviting alice@example.com as MANAGER now.



       (Agent calls admin.confirm_action then team.invite_member.)



       Invite sent to alice@example.com. They'll receive an email to

       accept the invite and join the workspace.
```

## What requires admin code-paste[​](#what-requires-admin-code-paste "Direct link to What requires admin code-paste")

| Tool                           | Action string                  | Subject              |
| ------------------------------ | ------------------------------ | -------------------- |
| `team.invite_member`           | `team.invite_member`           | invitee email        |
| `team.remove_member`           | `team.remove_member`           | user id              |
| `team.change_role`             | `team.change_role`             | `<userId>:<newRole>` |
| `billing.cancel_subscription`  | `billing.cancel`               | (none)               |
| `billing.resume_subscription`  | `billing.resume`               | (none)               |
| `billing.change_plan`          | `billing.change_plan`          | priceId              |
| `api_key.create`               | `api_key.create`               | requested scopes     |
| `api_key.revoke`               | `api_key.revoke`               | key id               |
| `workspace.delete`             | `workspace.delete`             | (none)               |
| `workspace.transfer_ownership` | `workspace.transfer_ownership` | new owner userId     |

Self-revoke for API keys is a special case: pass `confirmSelf: true` and **no admin\_token** to revoke the calling key itself. Used for compromise mitigation when the user can't reach an alternate key.

## Failure modes[​](#failure-modes "Direct link to Failure modes")

| Error                            | Cause                                            | Resolution                                                     |
| -------------------------------- | ------------------------------------------------ | -------------------------------------------------------------- |
| `wrong_code`                     | Mistyped digits                                  | Try again. 5 wrong attempts consumes the request — start over. |
| `too_many_attempts`              | 5 wrong attempts                                 | Request a new code.                                            |
| `expired`                        | 10 minutes elapsed since request                 | Request a new code.                                            |
| `consumed`                       | Code already used or token already consumed      | Request a new code.                                            |
| `wrong_key`                      | Token issued to a different API key              | Verify you're using the right key.                             |
| `wrong_action` / `wrong_subject` | Mismatch between code-paste and destructive call | Restart the flow with matching values.                         |

## Browser-controlling agents[​](#browser-controlling-agents "Direct link to Browser-controlling agents")

Agents with computer use (Claude with computer use, OpenClaw, etc.) can:

* Navigate to the user's email inbox in a browser tab
* Read the latest message from `noreply@funnelfizz.com`
* Extract the 6-digit code
* Paste it back into the chat / call `admin.confirm_action` directly

The protocol is identical. Browser automation just removes the human-in-the-loop step.

## Related[​](#related "Direct link to Related")

* [Safety tiers](https://docs.funnelfizz.com/ai-agents/safety-tiers.md) — T0/T1/T2 explainer.
* [MCP](https://docs.funnelfizz.com/ai-agents/mcp.md) — main MCP overview.
* [Mutations](https://docs.funnelfizz.com/ai-agents/mutations.md) — T1 funnel + entity target\_token flows.
