What’s new in v2
- One-step auth.
X-API-Keyissued 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 / reclaim / rename / remove lets an active member take over a deactivated colleague’s data.
- Stable identifier. Every team member has a
team_user_idyou can cache; safer thanemailfor delegated profiles whose email is rewritten to a syntheticdelegate-<id>@...address. - Filters.
status_filteranddelegation_stateon 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> |
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 |
USER_STATUS_REMOVED which behaves like calling 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, ...}.
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
Issue a v2 API key
Create a Team User Management key in Compliance settings. Copy it immediately — it is shown only once. See v2 Authentication.
Pilot a read-only call against v2
v1 and v2 hit the same backing tables, so you can exercise team.user.list against your live team without affecting v1 traffic. Confirm field names, role/status enum values, and pagination behavior.
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.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.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.Cut over and revoke v1 credentials
Once your integration is fully on v2, delete the old
client_id / client_secret via team.credential.delete so the v1 OAuth surface can no longer be used by stale callers.What stays the same
- Pagination defaults (
limit100, max 1000;offset0-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:- Swap the Token URL (
/api/user/manage/v1/oauth/token) for anX-API-Keystatic header — no token endpoint to call. - Update endpoint paths and method for update (PATCH → POST).
- Remap attribute paths under
users[](camelCase → snake_case) and any role/status string lookups to the new enum constants. - Optionally map
team_user_idas a secondary identifier alongsideemailso account aggregation continues to work for delegated profiles.