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"
}

Response204 No Content on success.

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 |


OAuth Consents

List consents

GET /api/v1/auth/consents

Returns all active OAuth consent grants for the authenticated user.

Response

[
  {
    "id": "consent_01j...",
    "client_name": "Acme Dashboard",
    "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.

Response204 No Content


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

{
  "access_token": "eyJhbGci...",
  "refresh_token": "rt_01j...",
  "expires_in": 900,
  "token_type": "Bearer"
}

| Field | Type | Description | |-------|------|-------------| | 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) | | token_type | string | Always "Bearer" |

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.