Skip to main content

v2 Memberships API

Phase A introduces a versioned /uflow/v2/... namespace for the new object-first endpoints. The legacy /uflow/admin/... endpoints remain for backward compatibility.

All endpoints require an admin bearer token (Authorization: Bearer <jwt>) and pass ValidateTenantFromToken middleware. Returns are JSON.

Tenant memberships

List members

GET /uflow/v2/tenants/:tenant_id/memberships

Query parameters:

ParamValuesEffect
statusactive, invited, suspended, leftFilter by lifecycle state
typeowner, admin, member, contractor, service_operator, readonly_auditorFilter by membership type

Response: {items: TenantMembership[], count: number}

Create membership

POST /uflow/v2/tenants/:tenant_id/memberships
Content-Type: application/json

{
"user_id": "<uuid>",
"membership_type": "member", // optional, defaults to "member"
"status": "active", // optional, defaults to "active"
"source": "api", // optional, defaults to "api"
"external_id": "okta:abc123" // optional
}

Idempotent on (tenant_id, user_id). Returns the created or existing row.

Get / update / delete one

GET    /uflow/v2/tenants/:tenant_id/memberships/:user_id
PATCH /uflow/v2/tenants/:tenant_id/memberships/:user_id
DELETE /uflow/v2/tenants/:tenant_id/memberships/:user_id

PATCH body accepts any subset of status, membership_type, external_id. Setting status to suspended records suspended_at.

Tenant end-user states

List end users

GET /uflow/v2/tenants/:tenant_id/end-users

Query parameters:

ParamValuesEffect
statusactive, suspendedFilter by state
plan_tierany stringFilter by plan
qsubstringMatch email or username (ILIKE)

Response: {items, count} where each item includes joined user_email and user_username for convenience.

Get / update one

GET   /uflow/v2/tenants/:tenant_id/end-users/:user_id
PATCH /uflow/v2/tenants/:tenant_id/end-users/:user_id

PATCH body:

{
"status": "active|suspended",
"plan_tier": "free|pro|<custom>|null",
"rate_limit_override": "<json string>",
"suspended_reason": "abuse: too many failed tool calls"
}

If no row exists yet, PATCH upserts one with default status='active'.

Suspend / reactivate (convenience)

POST /uflow/v2/tenants/:tenant_id/end-users/:user_id/suspend
{
"reason": "free-text"
}

POST /uflow/v2/tenants/:tenant_id/end-users/:user_id/reactivate

Group-subject role bindings

Bind a group to a role

POST /uflow/v2/groups/:group_id/role-bindings
Content-Type: application/json

{
"tenant_id": "<uuid>",
"role_id": "<uuid>",
"scope_type": "resource_server", // optional; null/"*" = tenant-wide
"scope_id": "<uuid>", // optional
"conditions": { /* free-form jsonb, Phase B enforces */ },
"expires_at": "2026-12-31T23:59:59Z" // optional
}

Returns the created RoleBinding row.

Effective access

GET /uflow/v2/users/:user_id/effective-access

Returns every binding currently affecting the user — direct + via group. Expired bindings (expires_at <= NOW()) are excluded.

Response item shape:

{
binding_id: string; // uuid
role_id: string; // uuid
role_name: string;
subject: "user" | "group" | "service_account";
subject_id: string; // uuid
scope_type: string | null;
scope_id: string | null;
expires_at: string | null;
}

Use this endpoint to power "why does this user have access?" diagnostics — same backing data as the Effective Access page in the admin UI.