Admin Endpoints

Platform administration — stats, user management, and session oversight.


Authentication

/api/v1/admin/stats and /api/v1/admin/sessions endpoints require a Bulwark-issued JWT with the admin role.

/api/v1/admin/users and /api/v1/admin/users/{id} endpoints accept either:

  • a JWT with the admin role (dashboard / MCP use), OR
  • an API key carrying the matching scope — users:read for list/get, users:write for create/update/delete.

/api/v1/admin/users/{id}/sessions endpoints (list, revoke one, revoke all) follow the same dual-auth model with the sessions:read / sessions:write scopes. Use these to power "Active sessions" UIs in your tenant portal.

API-key auth is the path backend services should use (e.g. auto-creating users from an invitation-accept flow on a consumer app, or rendering an account-security page that lists devices). Send the key as Authorization: Bearer bwk_live_... plus X-Bulwark-Tenant: <workspace-id>.

Keys are created + scoped via POST /api/v1/api-keys or the admin dashboard's picker.


Platform Stats

GET /api/v1/admin/stats

Headers

  • Authorization: Bearer <adminToken>
  • X-Bulwark-Tenant: <tenant-id>
  • X-Bulwark-App-Id: <app-id> — Optional. Filters results to a specific application.

Response 200

{
  "total_users": 1240,
  "active_users": 892,
  "suspended_users": 5,
  "total_agents": 78,
  "active_agents": 64,
  "active_sessions": 203,
  "events_24h": 48920
}

List Users

GET /api/v1/admin/users

Query Parameters

  • limit — Results per page (default: 25)
  • offset — Pagination offset (default: 0)
  • search — Search by email or name
  • status — Filter by status (active, suspended)
  • mfa — Filter by TOTP enrollment (enrolled, not_enrolled)

Response 200

{
  "users": [
    {
      "id": "usr_01j...",
      "tenant_id": "019d...",
      "email": "[email protected]",
      "email_verified": true,
      "display_name": "Jane Doe",
      "status": "active",
      "roles": ["user"],
      "created_at": "2026-01-15T00:00:00Z",
      "updated_at": "2026-01-15T00:00:00Z",
      "last_login_at": "2026-03-17T22:00:00Z",
      "credential_count": 1,
      "session_count": 2,
      "mfa_enrolled": false
    }
  ],
  "count": 1,
  "offset": 0,
  "limit": 25
}

Get User

GET /api/v1/admin/users/{id}

Response 200

{
  "id": "usr_01j...",
  "tenant_id": "019d...",
  "email": "[email protected]",
  "email_verified": true,
  "display_name": "Jane Doe",
  "status": "active",
  "roles": ["user"],
  "created_at": "2026-01-15T00:00:00Z",
  "updated_at": "2026-01-15T00:00:00Z",
  "last_login_at": "2026-03-17T22:00:00Z",
  "credential_count": 1,
  "session_count": 2,
  "mfa_enrolled": false
}

Create User

POST /api/v1/admin/users

Creates a user in the workspace's user pool. Intended for programmatic provisioning from a consumer app — e.g. auto-creating a Bulwark user the moment an invitation is accepted, so the invitee doesn't have to complete a separate signup.

Auth

Requires users:write scope (for API key) or admin role (for JWT).

Body

{
  "email": "[email protected]",
  "password": "optional-initial-password",
  "display_name": "New User",
  "email_verified": true,
  "roles": ["user"]
}
  • email (required) — must be unique within the workspace user pool.
  • password (optional) — if omitted, the user has no login credential and must complete a password-reset (or be provisioned a WebAuthn / passkey credential) before they can sign in.
  • display_name, email_verified, roles — optional. roles defaults to ["user"].

Response 201

Returns the full user detail — the caller can map the returned id to their own user record.

{
  "id": "usr_01j...",
  "tenant_id": "019d...",
  "email": "[email protected]",
  "email_verified": true,
  "display_name": "New User",
  "status": "active",
  "roles": ["user"],
  "created_at": "2026-04-16T16:00:00Z",
  "updated_at": "2026-04-16T16:00:00Z",
  "credential_count": 0,
  "session_count": 0,
  "mfa_enrolled": false
}

Errors

  • 400 — missing or invalid email.
  • 409 — email already exists in the workspace user pool.
  • 401 / 403 — authentication or scope/role check failed.

Update User

PATCH /api/v1/admin/users/{id}

All fields are optional — send only the fields you want to change.

Body

{
  "email": "[email protected]",
  "status": "suspended",
  "roles": ["admin"]
}
  • email (optional) — new login email; must be unique within the workspace.
  • status (optional) — active or suspended.
  • roles (optional) — replaces the user's role list.

Response 200

{ "status": "updated" }

Delete User

DELETE /api/v1/admin/users/{id}

Requires users:write scope (API key) or admin role (JWT).

Response 200

{ "status": "deleted" }

Returns 404 if the user does not exist.


Reset User MFA

DELETE /api/v1/admin/users/{id}/mfa

Removes a user's TOTP devices and any remaining backup codes — e.g. when a user has lost their authenticator. Admin JWT.

Response 200

{
  "status": "reset",
  "totp_removed": 1,
  "backup_codes_removed": 8
}

Returns 501 if the MFA service is not configured for this deployment.


List User Consents

GET /api/v1/admin/users/{id}/consents

Lists the user's active OAuth2 consent grants (which connected apps they've authorized). Admin JWT.

Response 200

{
  "consents": [
    {
      "id": "cns_01j...",
      "client_id": "bwk_...",
      "client_name": "Acme Data Exporter",
      "description": "Exports customer reports",
      "scopes": ["read:customers"],
      "granted_at": "2026-05-01T00:00:00Z"
    }
  ]
}

description is omitted when the client has none.


Revoke User Consent

DELETE /api/v1/admin/users/{id}/consents/{consent_id}

Revokes one consent grant, cutting off the connected app's access on the user's behalf. Admin JWT.

Response 200

{ "status": "revoked" }

Returns 422 if the consent can't be revoked (e.g. not found).


List User Login Sessions

One row per signed-in device. Each row corresponds to a refresh-token family. Device fields (browser, os, device_type) are parsed server-side from the User-Agent captured at login. Geo fields (location.city, region, country) are populated from MaxMind GeoLite2 when configured; absent otherwise.

GET /api/v1/admin/users/{id}/sessions

Authentication

Either an admin JWT or an API key with the sessions:read scope.

Headers

  • Authorization: Bearer <adminToken|bwk_live_...>
  • X-Bulwark-Tenant: <tenant-id>

Response 200

{
  "sessions": [
    {
      "id": "0193d07e-d8cb-7819-b0a1-48c90e602461",
      "family_id": "0193d07e-aaaa-7819-b0a1-48c90e602461",
      "ip_address": "203.0.113.42",
      "user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) ...",
      "browser": "Chrome",
      "browser_version": "125",
      "os": "macOS",
      "os_version": "14.5",
      "device_type": "desktop",
      "location": {
        "city": "Salt Lake City",
        "region": "UT",
        "country": "US"
      },
      "started_at": "2026-05-01T12:00:00Z",
      "last_active_at": "2026-05-09T08:30:00Z",
      "expires_at": "2026-05-31T12:00:00Z"
    }
  ],
  "count": 1
}

Field notes:

  • id is what you pass to the revoke endpoints below.
  • location is omitted entirely (not just empty) when no geo data is available, so the UI can hide the column cleanly.
  • last_active_at advances on every refresh-token rotation — useful as a "last seen" indicator.
  • Sessions created by users who logged in via passkey or OAuth/social currently do NOT appear here — those auth flows do not issue refresh tokens. Only password logins and refresh-token rotations populate the sessions table.

Revoke One User Session

Signs the user out of one specific device. The other devices stay signed in.

DELETE /api/v1/admin/users/{id}/sessions/{session_id}

Authentication

Either an admin JWT or an API key with the sessions:write scope.

Response 200

{ "status": "revoked" }

Returns 404 if the session id doesn't exist or doesn't belong to (user_id, tenant_id) — guards against cross-tenant id-guessing.


Revoke ALL User Sessions

Signs the user out of every device. Use for forced sign-out (compromise response, account suspension, etc.).

DELETE /api/v1/admin/users/{id}/sessions

Authentication

Either an admin JWT or an API key with the sessions:write scope.

Response 200

{
  "status": "revoked",
  "revoked": 4
}

revoked is the count of sessions that were active when the call landed.


List Agent Sessions

Different concept from user login sessions above. This lists active agent execution sessions (AI-agent runs against the credential proxy), not user devices. Most tenant-portal use cases want the user-login endpoint above.

GET /api/v1/admin/sessions

Authentication

Admin JWT only (no API key support on this endpoint).

Response 200

{
  "sessions": [
    {
      "id": "...",
      "agent_id": "...",
      "agent_name": "billing-sync",
      "status": "active",
      "ttl_seconds": 3600,
      "max_uses": 100,
      "current_uses": 23,
      "expires_at": "2026-05-09T13:00:00Z",
      "created_at": "2026-05-09T12:00:00Z"
    }
  ],
  "count": 1,
  "offset": 0,
  "limit": 25
}

Revoke Agent Session

DELETE /api/v1/admin/sessions/{id}

Immediately ends one agent execution session (from the list above). Admin JWT.

Response 200

{ "status": "revoked" }

Organization Settings

Org-wide configuration — auth methods, session TTLs, security policy, branding, and email templates. These are distinct from the per-application settings under /api/v1/settings/applications.

GET /api/v1/settings
PUT /api/v1/settings

Authentication

Both endpoints require a Bulwark-issued JWT with the admin role.

Response 200

{
  "settings": {
    "auth_methods": {
      "password_enabled": true,
      "passkeys_enabled": true,
      "mfa_policy": "optional"
    },
    "session_config": {
      "access_token_ttl": 900,
      "refresh_token_ttl": 2592000,
      "session_ttl": 720
    },
    "security_config": {
      "rate_limit_enabled": true,
      "email_verify_required": true,
      "registration_enabled": true,
      "audit_retention_days": 90
    },
    "branding": { "...": "see Branding endpoints" },
    "email_templates": { "...": "see Email Templates" }
  }
}

Audit retention

security_config.audit_retention_days controls how long audit events are kept before the daily cleanup worker deletes them.

  • Default: 90 days. A value of 0 means "use the default," not "keep forever."
  • Clamped to [7, 365] on write and in the SQL retention function — values outside the range are silently clamped, so retention can never be disabled or grow unbounded via the API.

PUT /api/v1/settings accepts a partial body; only the fields you send are updated.


Waitlist

A simple pre-launch waitlist. Joining is public; listing and removing entries require admin access.

Join the waitlist

POST /api/v1/waitlist

Public — requires only the X-Bulwark-Tenant header.

Body

{
  "email": "[email protected]",
  "name": "Jane Doe",
  "company": "Acme"
}

email is required; name and company are optional. Duplicate emails are ignored.

Response 200

{ "message": "You're on the list! We'll be in touch soon." }

Returns 400 for a missing or malformed email.

List entries

GET /api/v1/admin/waitlist

Admin JWT.

{
  "entries": [
    {
      "id": "wl_01j...",
      "email": "[email protected]",
      "name": "Jane Doe",
      "company": "Acme",
      "created_at": "2026-06-01T00:00:00Z"
    }
  ],
  "count": 1
}

name and company are omitted when empty.

Remove an entry

DELETE /api/v1/admin/waitlist/{id}

Admin JWT. Returns 200 { "status": "deleted" }, or 404 if the entry does not exist.


Application Isolation

Moves an application from shared-workspace mode to isolated mode, where it no longer shares users, agents, FGA tuples, API keys, or webhooks with the rest of the workspace. Preview first to see what becomes inaccessible, then isolate. Admin JWT.

Preview

GET /api/v1/admin/apps/{id}/isolation-preview

Advisory counts of workspace-scoped resources the app would lose access to after isolation.

Response 200

{
  "app_id": "ten_01j...",
  "app_name": "Acme Mobile",
  "workspace_id": "wsp_01j...",
  "current_mode": "shared",
  "workspace_users": 1240,
  "workspace_fga_tuples": 87,
  "workspace_agents": 12,
  "workspace_api_keys": 4,
  "workspace_webhooks": 2
}

Isolate

POST /api/v1/admin/apps/{id}/isolate

Flips the app to isolated mode.

Response 200

{
  "app_id": "ten_01j...",
  "new_mode": "isolated",
  "audit_id": "evt_01j..."
}

audit_id references the audit event recording the change.