Skip to main content

User management

The user-management endpoints let an org admin invite users, remove them, and pull consumption data from a script, automation, or AI agent — without logging into the Phoenix webapp.

All operations require an admin-scoped API key. User- scoped keys receive 403 forbidden_admin_scope on every endpoint and tool listed here.

Surfaces

The same three operations are exposed three ways:

SurfacePath / tool nameAuth
REST (admin facade)POST /api/admin/users/invite, DELETE /api/admin/users/{userId}, GET /api/admin/users/consumptionAuthorization: Bearer <admin_key> or x-api-key
MCP toolsadmin_invite_user, admin_remove_user, admin_get_consumptionAdmin-scoped API key over /api/mcp (Bearer) or /api/ai/{key}/mcp
Power Automate REST facadePOST /api/powerautomate/admin_invite_user (etc.)Authorization: Bearer <admin_key>

The REST and Power Automate paths share the same wire shape; the MCP path wraps each call in the JSON-RPC tools/call envelope. Underlying business logic is the same — see webapp/src/server/api/admin/user-management.ts.

Common headers

Authorization: Bearer phx_<your-admin-key>
Content-Type: application/json

x-api-key: phx_<your-admin-key> is also accepted (preferred for Azure APIM and Power Automate connectors).

All Phoenix API keys share the phx_ prefix; admin scope is set on the registry record, not derived from the key string. There is no visible "admin" suffix in the key itself — the Admin badge in the webapp keys table is the canonical marker.

Org isolation

The organization is always derived from the API key (webapp_api_keys_registry.organization_slug). It is never accepted from the request body, query, or path. An admin key for org A cannot read or modify org B's data; cross-org probes return 404 user_not_found or 403 forbidden_admin_scope.

Auditing

Every successful mutation writes a row to webapp_org_admin_audit_log (see overview). For user-management, you'll see actions invite_user, remove_user, and view_consumption. Failed attempts (403, 404, 409, 400) are NOT audited — the audit log records intent only after the operation completes.

Rate limiting

All admin endpoints share the standard MCP-tool rate limit (500 requests per minute per API key). Responses include X-RateLimit-Limit, X-RateLimit-Remaining, and X-RateLimit-Reset headers. Exceeding the limit returns 429 with a Retry-After header.


POST /api/admin/users/invite

Send an invitation email to a user. The recipient follows the magic-link to set a password and join the org. Does not create a user record directly.

Request body

{
"email": "newhire@acme.com",
"role": "member",
"name": "Jordan Lee"
}
FieldTypeRequiredDescription
emailstring (RFC 5322)YesEmail of the invitee. Lower-cased server-side. Max 254 chars.
role"member" | "admin"YesRole granted upon accepting the invitation.
namestringNoDisplay name (1–255 chars).

Successful response (200)

{
"invitationId": "inv_01HXY…",
"email": "newhire@acme.com",
"role": "member",
"expiresAt": "2026-05-05T16:00:00.000Z"
}

Idempotency

If an active invitation already exists for that email × default team, the existing invitation is returned with status 200. The audit log records the call with metadata.idempotent = true. No duplicate row is created and no second email is sent.

Error codes

StatuserrorTrigger
400invalid_requestBody failed Zod validation.
400disposable_emailEmail domain is on the disposable-email blocklist.
401unauthorizedMissing or invalid API key.
403forbidden_admin_scopeKey is user-scoped, or the user is no longer an org admin.
409already_memberA user with this email is already an active member of this org.
429rate_limit_exceededMore than 500 requests in 1 minute for this key.
500org_misconfiguredOrg has no default team (should never happen — file a bug).

Example

curl -sS -X POST https://phoenix.hginsights.com/api/admin/users/invite \
-H "Authorization: Bearer phx_…" \
-H "Content-Type: application/json" \
-d '{"email":"newhire@acme.com","role":"member"}'

DELETE /api/admin/users/{userId}

Remove a user from the calling org. Hard-deletes all of the user's team memberships in the org and revokes their access:

  • Deletes tenant apiKeys rows for the user.
  • Deletes webapp_api_keys_registry rows for (userId, organizationSlug).
  • Deletes oauth_tokens rows for (userId, organizationSlug).
  • Invalidates the org-access cache and MCP org-context cache.

The user's public.users record is not deleted — historical attribution (created by, audit log target, etc.) is preserved.

Path parameters

NameTypeDescription
userIdUUIDID of the user to remove.

Successful response (200)

{
"userId": "9a3a…",
"removedAt": "2026-04-29T16:42:11.000Z",
"removedMembershipsCount": 1
}

Concurrency safety

The count + delete pair runs inside a tenant transaction with a Postgres advisory lock (pg_advisory_xact_lock(hashtext(slug))). Concurrent removal attempts serialize on this lock, so the org-wide last-admin guard cannot be bypassed by a race.

Error codes

StatuserrorTrigger
400invalid_user_idPath parameter is not a UUID.
400cannot_remove_selfuserId matches the calling user.
400cannot_remove_owneruserId is the org owner (transfer ownership first).
400last_adminRemoving the user would leave the org with zero active admins.
401unauthorizedMissing or invalid API key.
403forbidden_admin_scopeKey is user-scoped, or the user is no longer an org admin.
404user_not_foundNo active membership for that userId in this org (also returned for cross-org userId values — do not rely on this to test for user existence in other orgs).
429rate_limit_exceededMore than 500 requests in 1 minute for this key.

Example

curl -sS -X DELETE https://phoenix.hginsights.com/api/admin/users/9a3a-… \
-H "Authorization: Bearer phx_…"

GET /api/admin/users/consumption

Read consumption (credits + tool calls) for the calling org.

Query parameters

NameTypeRequiredDescription
userIdUUIDNoIf provided, returns a per-user breakdown. Otherwise returns the org-wide ConsumptionStatus.
fromISO datetimeNoWindow start (default: org's current billing period start).
toISO datetimeNoWindow end (default: org's current billing period end).

The [from, to] window must not exceed 366 days.

Org-wide response (200, no userId)

Same shape as the existing webapp consumption view (ConsumptionStatus).

{
"organizationSlug": "acme",
"organizationName": "Acme",
"planId": "plan_growth",
"planName": "Growth",
"billingPeriod": {
"start": "2026-04-01T00:00:00.000Z",
"end": "2026-04-30T23:59:59.000Z"
},
"credits": {
"used": 1234.5,
"limit": 10000,
"remaining": 8765.5,
"percentUsed": 12.35
},
"overage": { "amount": 0, "cost": 0 },
"enforcementMode": "soft",
"isOverLimit": false,
"isCustomPricing": false
}

Per-user response (200, with userId)

{
"users": [
{
"userId": "9a3a…",
"email": "alice@acme.com",
"name": "Alice",
"callCount": 42,
"credits": 87.5,
"byTool": [
{ "toolName": "company_firmographic", "callCount": 30, "credits": 30 },
{ "toolName": "company_spend", "callCount": 12, "credits": 36 }
]
}
],
"from": "2026-04-01T00:00:00.000Z",
"to": "2026-04-30T23:59:59.000Z"
}

Cross-org isolation

When userId is provided, the user must be an active member of the caller's org. Otherwise the response is 404 user_not_found. This prevents an admin in org A from probing org B's user IDs.

Error codes

StatuserrorTrigger
400invalid_requestQuery params failed Zod validation.
400invalid_rangefrom > to.
400range_too_largeWindow exceeds 366 days.
401unauthorizedMissing or invalid API key.
403forbidden_admin_scopeKey is user-scoped, or the user is no longer an org admin.
404user_not_founduserId provided but not an active member of this org.
404org_not_foundCalling org's slug no longer resolves (should never happen — file a bug).

Example

# Org-wide
curl -sS "https://phoenix.hginsights.com/api/admin/users/consumption" \
-H "Authorization: Bearer phx_…"

# Per-user, last 7 days
curl -sS "https://phoenix.hginsights.com/api/admin/users/consumption?userId=9a3a-…&from=2026-04-22T00:00:00Z&to=2026-04-29T00:00:00Z" \
-H "Authorization: Bearer phx_…"

MCP tool calls

The same operations through the MCP tools/call envelope:

{
"jsonrpc": "2.0",
"id": "1",
"method": "tools/call",
"params": {
"name": "admin_invite_user",
"arguments": { "email": "newhire@acme.com", "role": "member" }
}
}

Tool names use the admin_ prefix:

  • admin_invite_user — input: { email, role, name? }
  • admin_remove_user — input: { user_id } (snake_case to match MCP convention)
  • admin_get_consumption — input: { user_id?, from?, to? }

Admin tools are only listed in tools/list for admin-scoped keys. A user-scoped key gets the standard tool catalog with no admin_* entries. The public tool catalog at /api/mcp/spec also omits admin tools.

Power Automate

Use the standard Power Automate REST facade — the same allowlist that exposes company_firmographic etc. is extended with the three admin tools. They show up in the connector's actions list, but calling them with a user-scoped key still returns 403 at request time. Discovery does not imply authorization.

See also