User API

Self-service endpoints for authenticated users to manage their own profile, credentials, and OAuth consents.

All endpoints require Authorization: Bearer <access_token>. The token is the user's own JWT — no admin role needed.

You may include the optional X-Bulwark-App-Id header to scope user activity to a specific application context.


Profile

Get profile

GET /api/v1/user/profile

Returns the authenticated user's profile.

Response

{
  "id": "usr_01j...",
  "email": "[email protected]",
  "email_verified": true,
  "display_name": "Jane Doe",
  "avatar_url": "https://cdn.bulwarkauth.com/avatars/usr_01j....png",
  "roles": ["user"],
  "created_at": "2026-01-15T09:00:00Z"
}

| Field | Type | Description | |-------|------|-------------| | id | string | Stable user identifier | | email | string | User's email address | | email_verified | boolean | Whether the email has been verified | | display_name | string \| null | User's display name | | avatar_url | string \| null | URL to profile avatar | | roles | string[] | Roles assigned to this user | | created_at | string | ISO 8601 creation timestamp |


Update profile

PATCH /api/v1/user/profile

Updates mutable profile fields. Only include fields you want to change.

Request body

| Field | Type | Description | |-------|------|-------------| | display_name | string | New display name | | avatar_url | string | New avatar URL |

{
  "display_name": "Jane D.",
  "avatar_url": "https://example.com/my-avatar.png"
}

Response — updated profile object (same shape as GET).


Password

Change password

POST /api/v1/user/change-password

Changes the authenticated user's password. Requires the current password for verification.

Request body

| Field | Type | Required | Description | |-------|------|----------|-------------| | current_password | string | Yes | The user's current password | | new_password | string | Yes | The new password |

Password requirements: 8–128 characters. No other constraints are enforced by default; configure policy in the dashboard.

{
  "current_password": "hunter2",
  "new_password": "correct-horse-battery-staple"
}

Response 200

{ "status": "password changed" }

Errors

| Status | Code | Description | |--------|------|-------------| | 400 | password_too_short | New password is fewer than 8 characters | | 400 | password_too_long | New password exceeds 128 characters | | 401 | invalid_credentials | current_password is incorrect |


Active Sessions

These endpoints let a user inspect and revoke their own signed-in devices — typically wired to a "Security" or "Active sessions" panel in your tenant portal. No admin role or API key required; the user authenticates with their own access token.

List my sessions

GET /api/v1/me/sessions

Returns one row per device the user is currently signed in on. The row matching the access token making the request is flagged with current: true so the UI can render "This device" and avoid surprising the user when they revoke.

Response

{
  "sessions": [
    {
      "id": "0193d07e-d8cb-7819-b0a1-48c90e602461",
      "family_id": "0193d07e-aaaa-7819-b0a1-48c90e602461",
      "current": true,
      "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 | Type | Notes | |-------|------|-------| | id | string | Pass to the revoke endpoints below | | current | boolean | Only set on the row the caller is signed in from | | location | object \| undefined | Omitted entirely when no geo data is available | | device_type | string \| null | One of desktop, mobile, tablet, bot |

The current flag depends on the access token carrying a sid claim. Tokens issued before the sessions feature was deployed (or via flows that don't issue refresh tokens, such as the dashboard-only login) lack sid — in those cases no row will be flagged. The list is still complete; the UI just won't be able to highlight "this device."


Revoke one session

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

Signs the user out of one specific device. If the id matches the caller's current session, the next refresh will return 401 and the SDK will redirect to login — that's intentional ("sign out from this device" is a valid action).

Response

{ "status": "revoked" }

| Status | Description | |--------|-------------| | 404 | Session id doesn't exist or doesn't belong to the calling user |


Revoke all OTHER sessions

DELETE /api/v1/me/sessions

Signs the user out of every device EXCEPT the one making the request. Common UX: a "Sign out everywhere else" button in account security settings, after a password change.

Response

{
  "revoked": 4,
  "kept": 1
}

kept is 1 when the caller had a sid claim and that session still exists, 0 otherwise (in which case ALL sessions were revoked, including the caller's — this matters for legacy tokens; document it in your UI).


OAuth Consents

List consents

GET /api/v1/auth/consents

Returns all active OAuth consent grants for the authenticated user.

Response

{
  "consents": [
    {
      "id": "consent_01j...",
      "client_id": "client_01j...",
      "scopes": ["read:profile", "read:email"],
      "granted_at": "2026-02-10T14:30:00Z"
    }
  ]
}

Revoke consent

DELETE /api/v1/auth/consents/{id}

Revokes an OAuth consent grant. The associated OAuth application will lose access on the next token refresh.

Response 200

{ "status": "consent revoked" }

Token refresh

Refresh access token

POST /api/v1/auth/refresh

Exchanges a refresh token for a new access token and refresh token. The old refresh token is invalidated after this call (token rotation).

Request body

| Field | Type | Required | Description | |-------|------|----------|-------------| | refresh_token | string | Yes | A valid refresh token |

{
  "refresh_token": "rt_01j..."
}

Response

{
  "user_id": "usr_01j...",
  "access_token": "eyJhbGci...",
  "refresh_token": "rt_01j...",
  "expires_in": 900,
  "email_verified": true
}

| Field | Type | Description | |-------|------|-------------| | user_id | string | Stable user identifier | | access_token | string | New JWT access token | | refresh_token | string | New refresh token (old one is invalidated) | | expires_in | integer | Access token lifetime in seconds (default: 900) | | email_verified | boolean | Whether the user's email has been verified |

Errors

| Status | Code | Description | |--------|------|-------------| | 401 | invalid_refresh_token | Token is expired, revoked, or malformed | | 401 | refresh_token_rotated | Token was already used (possible replay attack) |

Note: The SDKs handle token refresh automatically. Call this endpoint directly only if you are managing tokens without an SDK.