> ## Documentation Index
> Fetch the complete documentation index at: https://docs.manus.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Migrating from v1

> Move existing SailPoint, Okta, or custom IDP integrations from the v1 user management API to v2

<sup>Questions or issues? Contact us at [api-support@manus.ai](mailto:api-support@manus.ai).</sup>

v2 supersedes the v1 [Team User Management](/enterprise/v1/user-management-overview) surface. v1 is **not removed** — existing integrations keep working — but new integrations should target v2 directly, and existing ones benefit from a smaller blast radius for credential management, a unified response envelope, the profile-delegation workflow, and stronger field validation.

## What's new in v2

* **One-step auth.** `X-API-Key` issued from Compliance settings — no DevTools-extracted session token, no separate OAuth token endpoint, no 1-hour token rotation.
* **Single REST surface.** Every operation is `GET/POST /v2/team.user.*` with a unified `{ok, request_id, ...}` envelope. Connect RPC is also available at `/team.v2.TeamUserManagementApiV2Service/<Method>`.
* **Profile delegation.** [delegate](/enterprise/v2/team.user.delegate) / [reclaim](/enterprise/v2/team.user.reclaim) / [rename](/enterprise/v2/team.user.rename) / [remove](/enterprise/v2/team.user.remove) lets an active member take over a deactivated colleague's data.
* **Stable identifier.** Every team member has a `team_user_id` you can cache; safer than `email` for delegated profiles whose email is rewritten to a synthetic `delegate-<id>@...` address.
* **Filters.** `status_filter` and `delegation_state` on [team.user.list](/enterprise/v2/team.user.list).
* **Server-side validation.** Email format, length limits, and enum membership are enforced at the wire level via protovalidate; bad inputs fail fast with `invalid_argument`.

## Authentication

v1 used a two-step credential dance (session token → CreateApiCredential → `client_id` + `client_secret` → OAuth token endpoint → 1-hour Bearer access token). v2 collapses this to a single header.

| Step | v1                                                                   | v2                                                               |
| ---- | -------------------------------------------------------------------- | ---------------------------------------------------------------- |
| 1    | DevTools → copy session token                                        | (none)                                                           |
| 2    | RPC `CreateApiCredential` → `clientId` + `clientSecret` (shown once) | Compliance settings → create key (type **Team User Management**) |
| 3    | `POST /api/user/manage/v1/oauth/token` → `access_token` (1 h)        | (none)                                                           |
| 4    | `Authorization: Bearer <access_token>`                               | `X-API-Key: <your-api-key>`                                      |

See [v2 Authentication](/enterprise/v2/authentication) for the full flow.

## Endpoint mapping

| v1 endpoint                                                              | v2 endpoint                             | Notes                                                   |
| ------------------------------------------------------------------------ | --------------------------------------- | ------------------------------------------------------- |
| `GET /api/user/manage/v1/users?limit=&offset=`                           | `GET /v2/team.user.list?limit=&offset=` | Plus optional `status_filter` / `delegation_state`      |
| `GET /api/user/manage/v1/users/{email}`                                  | `GET /v2/team.user.detail?email=...`    | Or `?team_user_id=...`                                  |
| `POST /api/user/manage/v1/users`                                         | `POST /v2/team.user.create`             | Body shape similar (snake\_case)                        |
| `PATCH /api/user/manage/v1/users/{email}`                                | `POST /v2/team.user.update`             | Identifier moves into the body; method becomes POST     |
| (none)                                                                   | `POST /v2/team.user.delegate`           | New: hand a deactivated profile to an active member     |
| (none)                                                                   | `POST /v2/team.user.reclaim`            | New: return a delegated profile to the deactivated pool |
| (none)                                                                   | `POST /v2/team.user.rename`             | New: change a profile's display name                    |
| (none)                                                                   | `POST /v2/team.user.remove`             | New: hard-delete a member (cascades reclaim)            |
| RPC `CreateApiCredential` / `ListApiCredentials` / `DeleteApiCredential` | Compliance settings UI                  | Credential CRUD moves out of the API surface            |

## Field naming and enums

**Wire format.** v1 uses **camelCase** (`userName`, `firstName`, `lastName`); v2 uses **snake\_case** (`user_name`, `first_name`, `last_name`).

**Role.**

| v1 string            | v2 enum                                      |
| -------------------- | -------------------------------------------- |
| `"super_admin"`      | `TEAM_MEMBER_ROLE_SUPER_ADMIN`               |
| `"admin"`            | `TEAM_MEMBER_ROLE_ADMIN`                     |
| `"member"`           | `TEAM_MEMBER_ROLE_MEMBER`                    |
| `"free_tier_member"` | `TEAM_MEMBER_ROLE_GUEST`                     |
| `"owner"`            | `TEAM_MEMBER_ROLE_OWNER` (read-only in both) |

`free_tier_member` was renamed to **GUEST** in v2 to drop billing-internal jargon. Same semantics — does not consume a paid Stripe seat. Promoting `GUEST → MEMBER/ADMIN/SUPER_ADMIN` triggers a Stripe seat-quantity bump in both v1 and v2.

**Status.**

| v1 string       | v2 enum                                       |
| --------------- | --------------------------------------------- |
| `"active"`      | `USER_STATUS_ACTIVE`                          |
| `"inactive"`    | `USER_STATUS_INACTIVE`                        |
| (UI-only in v1) | `USER_STATUS_REMOVED` ← **new, irreversible** |

v2 adds `USER_STATUS_REMOVED` which behaves like calling [team.user.remove](/enterprise/v2/team.user.remove) — hard-deletes the user and cascades reclaim of any profiles delegated to them.

**User response shape.**

| Field                    | v1     | v2                                                         |
| ------------------------ | ------ | ---------------------------------------------------------- |
| `email` / `email`        | ✓      | ✓                                                          |
| `userName` / `user_name` | ✓      | ✓                                                          |
| `firstName` / `lastName` | ✓      | not returned (use `user_name`)                             |
| `status`                 | string | `UserStatus` enum                                          |
| `role`                   | string | `TeamMemberRole` enum                                      |
| `team_user_id`           | —      | ✓ stable handle                                            |
| `delegated_to`           | —      | ✓                                                          |
| `delegated_profiles[]`   | —      | ✓ (single-record responses only — list omits to avoid N+1) |
| `original_email`         | —      | ✓ (pre-delegation email)                                   |

## Response envelope

v1 returns the payload directly. v2 wraps every response in `{ok, request_id, ...}`.

<CodeGroup>
  ```json v1 success (HTTP 200) theme={null}
  {
    "users": [...],
    "total": 50,
    "limit": 100,
    "offset": 0
  }
  ```

  ```json v2 success (HTTP 200) theme={null}
  {
    "ok": true,
    "request_id": "req_abc123",
    "users": [...],
    "total": 50,
    "limit": 100,
    "offset": 0
  }
  ```

  ```json v2 error (HTTP 4xx) theme={null}
  {
    "ok": false,
    "request_id": "req_abc123",
    "error": {
      "code": "not_found",
      "message": "user not found"
    }
  }
  ```
</CodeGroup>

## Error code mapping

| v1 (UPPER\_SNAKE)                 | v2 (lower\_snake)                                                    |
| --------------------------------- | -------------------------------------------------------------------- |
| `USER_NOT_FOUND`                  | `not_found`                                                          |
| `USER_ALREADY_EXISTS`             | `already_exists`                                                     |
| `INVALID_REQUEST`                 | `invalid_argument`                                                   |
| `UNAUTHORIZED` / `INVALID_CLIENT` | `permission_denied`                                                  |
| `INTERNAL_ERROR`                  | `internal`                                                           |
| (none)                            | `failed_precondition` (e.g. owner-immutable, source-not-deactivated) |

## Migration checklist

<Steps>
  <Step title="Issue a v2 API key">
    Create a **Team User Management** key in [Compliance settings](https://manus.im/app#settings/compliance-api). Copy it immediately — it is shown only once. See [v2 Authentication](/enterprise/v2/authentication).
  </Step>

  <Step title="Pilot a read-only call against v2">
    v1 and v2 hit the same backing tables, so you can exercise [team.user.list](/enterprise/v2/team.user.list) against your live team without affecting v1 traffic. Confirm field names, role/status enum values, and pagination behavior.
  </Step>

  <Step title="Rewrite request bodies">
    Convert camelCase → snake\_case (`userName` → `user_name`, etc.). Replace role/status string literals with the new enum constants. Move identifiers from path to body for `team.user.update`.
  </Step>

  <Step title="Adopt the response envelope">
    Read `ok` / `error` instead of relying on HTTP status codes alone. Persist `request_id` in your logs to make support requests actionable.
  </Step>

  <Step title="Cache `team_user_id`">
    Whenever you create a member or list members, store the returned `team_user_id`. Use it for subsequent updates — it survives renames and delegation, unlike `email`.
  </Step>

  <Step title="Cut over and revoke v1 credentials">
    Once your integration is fully on v2, delete the old `client_id` / `client_secret` via [team.credential.delete](/enterprise/v1/team.credential.delete) so the v1 OAuth surface can no longer be used by stale callers.
  </Step>
</Steps>

## What stays the same

* Pagination defaults (`limit` 100, max 1000; `offset` 0-based)
* Owner is read-only via API
* Auto-provisioning of personal accounts during create
* All operations are audit-logged
* The Stripe seat-sync behavior on guest↔paid promotions

## SailPoint / Okta connector notes

If your IDP connector is wired against v1, the migration is mostly:

1. Swap the **Token URL** (`/api/user/manage/v1/oauth/token`) for an `X-API-Key` static header — no token endpoint to call.
2. Update endpoint paths and method for update (PATCH → POST).
3. Remap attribute paths under `users[]` (camelCase → snake\_case) and any role/status string lookups to the new enum constants.
4. Optionally map `team_user_id` as a secondary identifier alongside `email` so account aggregation continues to work for delegated profiles.
