Skip to content

Users Endpoints

Base path: /api/v1/users

All endpoints require a valid Authorization: Bearer <access_token> header. See Authentication for the full auth flow.


Role-Based Access Summary

Endpointmemberadminowner
GET /me
PATCH /me
PATCH /me/password
POST /me/photo
GET / (list team)
POST /invite
PATCH /:id/role
POST /:id/deactivate
POST /:id/reactivate
POST /:id/reset-password
DELETE /:id

GET /api/v1/users/me

Returns the authenticated user's own profile. password_hash is never included in any response.

Response — 200 OK

json
{
  "id": "<uuid>",
  "accountId": "<uuid>",
  "firstName": "Jane",
  "lastName": "Doe",
  "email": "jane@acme.com",
  "phone": "+254712345678",
  "role": "owner",
  "isVerified": true,
  "isActive": true,
  "profilePhotoUrl": "https://pub-xxx.r2.dev/photos/user-id.jpg",
  "lastLoginAt": "2025-01-01T00:00:00.000Z",
  "createdAt": "2025-01-01T00:00:00.000Z",
  "updatedAt": "2025-01-01T00:00:00.000Z"
}

PATCH /api/v1/users/me

Updates the authenticated user's profile fields. All fields are optional — supply only what you want to change.

Request Body

json
{
  "firstName": "Jane",
  "lastName": "Smith",
  "email": "jane.smith@acme.com",
  "phone": "+254712345678"
}
FieldTypeNotes
firstNamestring
lastNamestring
emailstringMust be unique; triggers re-verification if changed
phonestringOptional phone number

Response — 200 OK

Returns the updated user object (same shape as GET /me).


PATCH /api/v1/users/me/password

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

Request Body

json
{
  "currentPassword": "oldPassword123",
  "newPassword": "newPassword456"
}
FieldTypeRequiredNotes
currentPasswordstringYesMust match the stored bcrypt hash
newPasswordstringYesMinimum 8 characters

Response — 204 No Content

Errors

CodeCause
401 UnauthorizedcurrentPassword is incorrect
400 Bad RequestnewPassword under 8 characters

POST /api/v1/users/me/photo

Uploads a profile photo. The file is stored in Cloudflare R2 and the profile_photo_url field on the user is updated to the public R2 URL.

Content-Type: multipart/form-data

Request

FieldTypeNotes
fileFileJPEG, PNG, or WebP. Max 5 MB

Response — 200 OK

json
{
  "url": "https://pub-xxx.r2.dev/profile-photos/<user-id>.jpg"
}

Why Cloudflare R2?

R2 was chosen over AWS S3 for photo storage because:

  • Zero egress fees — files served via Cloudflare's CDN have no bandwidth charges
  • Same SDK — R2 is S3-compatible; the @aws-sdk/client-s3 library works unchanged
  • Consistent vendor — we already use Cloudflare for DNS, CDN, and Pages; reducing the number of vendors simplifies billing and IAM

Errors

CodeCause
400 Bad RequestFile too large or unsupported format

GET /api/v1/users

Lists all active team members belonging to the authenticated user's account. Deactivated members are excluded.

Response — 200 OK

json
[
  {
    "id": "<uuid>",
    "accountId": "<uuid>",
    "firstName": "Jane",
    "lastName": "Doe",
    "email": "jane@acme.com",
    "role": "owner",
    "isVerified": true,
    "isActive": true,
    "lastLoginAt": "2025-01-01T00:00:00.000Z",
    "createdAt": "2025-01-01T00:00:00.000Z"
  }
]

POST /api/v1/users/invite

Creates a new team member under the caller's account. An invitation email with credentials can optionally be sent.

Required role: owner or admin

Request Body

json
{
  "firstName": "John",
  "lastName": "Smith",
  "email": "john@acme.com",
  "password": "tempPassword1",
  "role": "member"
}
FieldTypeRequiredNotes
firstNamestringYes
lastNamestringYes
emailstringYesMust be unique across all users
passwordstringYesMinimum 8 characters
rolestringYesadmin or member — cannot invite owner

Response — 201 Created

Returns the created user object.

Errors

CodeCause
403 ForbiddenCaller is a member and lacks permission
409 ConflictEmail already registered
422 UnprocessableValidation failure

PATCH /api/v1/users/:id/role

Changes the role of another team member. The owner role cannot be changed.

Required role: owner only

Path Parameters

ParameterDescription
idUUID of the user whose role is being changed

Request Body

json
{
  "role": "admin"
}

Allowed values: admin, member

Response — 200 OK

Returns the updated user object.

Errors

CodeCause
403 ForbiddenCaller is not owner
400 Bad RequestAttempting to change the owner's role
404 Not FoundUser not found in this account

POST /api/v1/users/:id/deactivate

Deactivates a team member. Deactivated users cannot log in. Their data is preserved.

Required role: owner only

Response — 200 OK

Returns the updated user object with isActive: false.

Errors

CodeCause
400 Bad RequestAttempting to deactivate yourself or the owner
404 Not FoundUser not in this account

POST /api/v1/users/:id/reactivate

Reactivates a previously deactivated team member.

Required role: owner only

Response — 200 OK

Returns the updated user object with isActive: true.


POST /api/v1/users/:id/reset-password

Admin-initiated password reset. Sets a temporary password on behalf of a team member. The member should change it on next login.

Required role: owner only

Request Body

json
{
  "newPassword": "TempPass123!"
}

Response — 200 OK

json
{
  "message": "Password reset successfully."
}

Errors

CodeCause
400 Bad RequestnewPassword under 8 characters or resetting own password
404 Not FoundUser not found in this account

DELETE /api/v1/users/:id

Permanently removes a team member from the account.

Required role: owner only

Response — 204 No Content

Errors

CodeCause
403 ForbiddenCaller is not owner
400 Bad RequestAttempting to remove the owner or yourself
404 Not FoundUser not found in this account

Internal use only — Sema Link Engineering