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:
| Param | Values | Effect |
|---|---|---|
status | active, invited, suspended, left | Filter by lifecycle state |
type | owner, admin, member, contractor, service_operator, readonly_auditor | Filter 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:
| Param | Values | Effect |
|---|---|---|
status | active, suspended | Filter by state |
plan_tier | any string | Filter by plan |
q | substring | Match 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.