TMA CloudTMA Cloud
API Reference

Users API

User management endpoints for TMA Cloud.

User management endpoints for TMA Cloud.

Note: All endpoints in this section use the general API rate limit (10000 requests per 15 minutes, per user when authenticated). Most also require admin privileges (first user). The sub-user endpoints require an account owner instead — see Sub-users.

List Users

GET /api/user/all

List users one account page at a time (admin only). Every returned owner is followed by all sub-users belonging to that account.

Query Parameters:

  • limit - Number of owner accounts, from 1 to 100 (optional, defaults to 50)
  • cursor - Opaque nextCursor from the previous response

Response:

An object containing the current account page and the next cursor. nextCursor is null after the final page.

{
  "users": [
    {
      "id": "user_123",
      "email": "user@example.com",
      "name": "User Name",
      "createdAt": "2024-01-01T00:00:00Z",
      "mfaEnabled": false,
      "storageUsed": 1073741824,
      "storageLimit": 107374182400,
      "storageTotal": 107374182400,
      "parentUserId": null,
      "permissions": null
    }
  ],
  "nextCursor": "eyJjcmVhdGVkQXQiOiIyMDI0LTAxLTAxVDAwOjAwOjAwLjAwMFoiLCJpZCI6InVzZXJfMTIzIn0"
}

Fields:

  • parentUserId: The account this login belongs to. null for top-level accounts.
  • permissions: Granted permissions for sub-users. null for owners, who hold all of them.

Sub-users

A sub-user is an extra login that shares the owner's files, folders and storage quota. These endpoints are restricted to account owners; a sub-user calling them receives 403.

See Authorization for the permission list and Sub-users for the interface.

GET /api/user/sub-users

List the current owner's sub-users, along with the permissions the server recognises.

Response:

{
  "subUsers": [
    {
      "id": "user_456",
      "email": "colleague@example.com",
      "name": "Colleague Name",
      "permissions": ["files.download", "files.upload"],
      "createdAt": "2024-01-01T00:00:00Z",
      "mfaEnabled": false
    }
  ],
  "availablePermissions": [
    {
      "key": "files.download",
      "label": "Download",
      "description": "Download files and folders, and open them in the document viewer"
    }
  ]
}

Note: availablePermissions is the full catalog in display order. Clients should render this rather than a hard-coded list, so the options always match what the server enforces.

POST /api/user/sub-users

Create a sub-user under the current owner.

Request Body:

{
  "email": "colleague@example.com",
  "password": "securepassword",
  "name": "Colleague Name",
  "permissions": ["files.download", "files.upload"]
}

Validation:

  • email: Required. Valid email, max 254 characters. Must not already exist on the instance.
  • password: Required. Between 8 and 128 characters.
  • name: Required. Non-empty after trimming, max 100 characters.
  • permissions: Required. Array of permission keys. May be empty. Unknown keys are rejected.

Response (201):

{
  "subUser": {
    "id": "user_456",
    "email": "colleague@example.com",
    "name": "Colleague Name",
    "permissions": ["files.download", "files.upload"],
    "createdAt": "2024-01-01T00:00:00Z",
    "mfaEnabled": false
  }
}

Error cases:

  • 403 Only the account owner can perform this action. - Caller is a sub-user
  • 409 Email already in use
  • 422 Validation failed - Missing name, short password, or an unknown permission key; details names the field

PUT /api/user/sub-users/:id

Replace a sub-user's permissions. The array is the complete new set, not a delta.

Request Body:

{
  "permissions": ["files.download"]
}

Validation:

  • permissions: Required. Array of permission keys. May be empty. Unknown keys are rejected.

Response:

{
  "subUser": {
    "id": "user_456",
    "email": "colleague@example.com",
    "name": "Colleague Name",
    "permissions": ["files.download"],
    "createdAt": "2024-01-01T00:00:00Z",
    "mfaEnabled": false
  }
}

Note: Changes apply to the sub-user's next request. They do not need to log in again.

DELETE /api/user/sub-users/:id

Remove a sub-user. Files are not affected — they belong to the owner. The sub-user's sessions and presence heartbeats are dropped immediately.

Response:

{
  "message": "Sub-user removed"
}

Error cases:

  • 403 Only the account owner can perform this action. - Caller is a sub-user
  • 404 Sub-user not found - No such sub-user under this owner

Storage

GET /api/user/storage

Get storage usage information for the authenticated user's account. For a sub-user this reports the owner's usage and limit, since the quota is shared.

Response:

  • S3: used; total and free are per-user limit and (limit − used), or null when no limit (Unlimited).
{
  "used": 1073741824,
  "total": 107374182400,
  "free": 106300440576
}

When S3 and no limit set: total and free may be null (Unlimited).

PUT /api/user/storage-limit

Update a user's storage limit (admin only).

Request Body:

{
  "targetUserId": "user_123",
  "storageLimit": 107374182400
}

Validation:

  • targetUserId: Required. Must be a string.
  • storageLimit: Optional. Must be a positive integer or null to reset to the default limit.

Response:

{
  "storageLimit": 107374182400
}

Signup Status

GET /api/signup-status

Public endpoint. No authentication. Use to show or hide the signup link on the login page.

Response:

{
  "signupEnabled": true
}

GET /api/user/signup-status

Requires authentication. Returns signup status, hide file extensions setting, and whether the current user can toggle them. The first user (admin) also receives totalUsers and additionalUsers.

Response (any authenticated user):

{
  "signupEnabled": true,
  "canToggle": false,
  "hideFileExtensions": false,
  "canToggleHideFileExtensions": false,
  "electronOnlyAccess": false,
  "canToggleElectronOnlyAccess": false,
  "allowPasswordChange": false,
  "canToggleAllowPasswordChange": false
}

Response (first user / admin):

{
  "signupEnabled": true,
  "canToggle": true,
  "totalUsers": 3,
  "additionalUsers": 2,
  "hideFileExtensions": false,
  "canToggleHideFileExtensions": true,
  "electronOnlyAccess": false,
  "canToggleElectronOnlyAccess": true,
  "allowPasswordChange": true,
  "canToggleAllowPasswordChange": true
}

POST /api/user/signup-toggle

Enable or disable public user signup (admin only).

Request Body:

{
  "enabled": true
}

Validation:

  • enabled: Required. Must be a boolean.

Response:

{
  "signupEnabled": true
}

OnlyOffice Configuration

GET /api/user/onlyoffice-configured

Check if OnlyOffice is configured on the server. This endpoint is accessible to all authenticated users and only indicates if the integration is active.

Response:

{
  "configured": true
}

GET /api/user/onlyoffice-config

Get the current OnlyOffice configuration (admin only). This does not expose the JWT secret.

Response:

{
  "jwtSecretSet": true,
  "url": "https://onlyoffice.example.com"
}

PUT /api/user/onlyoffice-config

Update the OnlyOffice configuration (admin only).

Request Body:

{
  "jwtSecret": "your_jwt_secret",
  "url": "https://onlyoffice.example.com"
}

Validation:

  • jwtSecret: Optional. Must be a string.
  • url: Optional. Must be a valid URL.

Note: Both jwtSecret and url must be provided together, or both must be empty/null to disable the integration.

Response:

The updated OnlyOffice configuration status.

{
  "jwtSecretSet": true,
  "url": "https://onlyoffice.example.com"
}

Share Base URL Configuration

GET /api/user/share-base-url-config

Get the share base URL configuration (admin only).

Response:

{
  "url": "https://share.example.com"
}

PUT /api/user/share-base-url-config

Update the share base URL configuration (admin only).

Request Body:

{
  "url": "https://share.example.com"
}

Validation:

  • url: Optional. Must be a valid URL.

Note: Set url to null to clear the configuration and use the request origin instead.

Response:

The updated share base URL configuration.

{
  "url": "https://share.example.com"
}

Max Upload Size Configuration

GET /api/user/max-upload-size-config

Get the current max upload size. Accessible to any authenticated user (used by the frontend for validation).

Response:

{
  "maxBytes": 10737418240
}

PUT /api/user/max-upload-size-config

Update the max upload size (admin only).

Request Body:

{
  "maxBytes": 5368709120
}

Validation:

  • maxBytes: Required. Integer between 1048576 (1 MB) and 107374182400 (100 GB).

Response:

{
  "maxBytes": 5368709120
}

Known Proxies Configuration

GET /api/user/known-proxies-config

Get the reverse proxies trusted to supply forwarded client IP headers (admin only).

Response:

{
  "knownProxies": ["10.1.2.100", "172.18.0.0/16", "proxy.example.com"]
}

PUT /api/user/known-proxies-config

Replace the known proxy list (admin only).

Request Body:

{
  "knownProxies": ["10.1.2.100", "proxy.example.com"]
}

Validation:

  • knownProxies: Required. Array with at most 100 entries.
  • Each entry must be an IP address, CIDR range, or hostname with at most 253 characters.

Response:

{
  "knownProxies": ["10.1.2.100", "proxy.example.com"],
  "restartRequired": true
}

The backend loads this list at startup. Restart every backend instance after an update.

Hide File Extensions Configuration

GET /api/user/hide-file-extensions-config

Get the current hide file extensions setting. Accessible to any authenticated user (used by the frontend for display).

Response:

{
  "hideFileExtensions": false
}

PUT /api/user/hide-file-extensions-config

Update the hide file extensions setting (admin only). When true, file names are shown without extensions in the file manager and rename dialog.

Request Body:

{
  "hidden": true
}

Validation:

  • hidden: Required. Must be a boolean.

Response:

{
  "hideFileExtensions": true
}

Desktop App Only Access Configuration

GET /api/user/electron-only-access-config

Get the desktop-only access setting. Accessible to any authenticated user.

Response:

{
  "electronOnlyAccess": false
}

PUT /api/user/electron-only-access-config

Update the desktop-only access setting (admin only). When enabled, the backend blocks browser access to the main app. Share links, /health, and /metrics remain available.

Request Body:

{
  "enabled": true
}

Validation:

  • enabled: Required. Must be a boolean.

Response:

{
  "electronOnlyAccess": true
}

Password Change Configuration

GET /api/user/password-change-config

Get the current password change setting. Accessible to any authenticated user (used by the frontend for display).

Response:

{
  "allowPasswordChange": true
}

PUT /api/user/password-change-config

Update the password change setting (admin only). When enabled, users can change their password from SettingsSecurity.

Request Body:

{
  "enabled": true
}

Validation:

  • enabled: Required. Must be a boolean.

Response:

{
  "allowPasswordChange": true
}

Active Desktop Clients

GET /api/user/active-clients

List active Electron desktop clients seen in the last 5 minutes (first user/admin only).

Response:

{
  "clients": [
    {
      "id": "user_123:client_abc",
      "userId": "user_123",
      "userName": "User Name",
      "userEmail": "user@example.com",
      "appVersion": "X.Y.Z",
      "platform": "win32",
      "ipAddress": "::ffff:192.168.1.1",
      "lastSeenAt": "2026-04-07T10:41:00.000Z",
      "connectedSince": "2026-04-07T10:20:00.000Z"
    }
  ]
}

POST /api/user/client-heartbeat

Record or refresh an Electron desktop client heartbeat (authenticated users).

This endpoint supplies the desktop rows shown by GET /api/user/active-clients and the heartbeat used by Active Sessions. Browser presence uses POST /api/sessions/heartbeat instead.

Request Body:

{
  "appVersion": "X.Y.Z",
  "platform": "win32",
  "clientId": "0ca7c806-8ee8-4fde-80e1-f2f43e5f9bfa"
}

Validation:

  • appVersion: Required. String.
  • platform: Optional. String.
  • clientId: Optional. String. When omitted, backend falls back to token sessionId.

Response:

{
  "ok": true
}

Orphaned Files

First user (admin) only. Any other caller receives 403 Only the first user can review orphaned files, and the attempt is audited.

GET /api/user/orphans

Queue a read-only storage/database scan on the background worker. Nothing is deleted.

Query Parameters:

  • graceMinutes: Optional. Integer between 60 and 525600 (1 year). Defaults to 1440 (24 hours). Items younger than this are held back on both sides so in-flight uploads are never reported.
  • jobId: Poll a scan started by an earlier request. Do not send it with graceMinutes.

Initial response (202):

{
  "jobId": "2b9eec75-29a7-4aeb-a177-a52d4c497f2a",
  "status": "created"
}

Poll GET /api/user/orphans?jobId=<jobId>. A running job returns 202; a completed job returns the report below.

Response:

{
  "scannedAt": "2026-08-12T14:05:00.000Z",
  "graceMinutes": 1440,
  "driver": "s3",
  "totals": {
    "storedObjects": 12043,
    "databaseRows": 12038,
    "skippedTooRecent": 3
  },
  "storageOrphans": {
    "items": [
      {
        "key": "9f2c1b7a-3c5e-4f11-8a90-2b0d5c7e1a44",
        "size": 5242880,
        "lastModified": "2026-08-01T09:12:00.000Z"
      }
    ],
    "count": 1,
    "totalBytes": 5242880,
    "truncated": false
  },
  "databaseOrphans": {
    "items": [
      {
        "id": "file_123",
        "name": "report.pdf",
        "path": "3a1f9c0d-77b2-4e6a-9d31-8c5b2e4f0a17",
        "size": 1048576,
        "mimeType": "application/pdf",
        "modified": "2026-07-20T11:00:00.000Z",
        "createdAt": "2026-07-20T11:00:00.000Z",
        "trashed": false,
        "ownerEmail": "user@example.com",
        "ownerName": "User Name"
      }
    ],
    "count": 1,
    "totalBytes": 1048576,
    "truncated": false
  }
}

Fields:

  • driver: s3.
  • totals.skippedTooRecent: Entries held back because they are younger than the grace window.
  • storageOrphans: Objects in storage with no files row pointing at them.
  • databaseOrphans: files rows whose stored object is missing. Trashed rows are included and flagged with trashed.
  • truncated: true when count exceeds the number of returned items. Each category returns at most 2000 items.

POST /api/user/orphans/delete

Queue deletion of the named orphans on the background worker. Each entry is re-verified against the database and storage before removal; entries that no longer qualify come back as skipped with a reason.

Request Body:

{
  "storageKeys": ["9f2c1b7a-3c5e-4f11-8a90-2b0d5c7e1a44"],
  "fileIds": ["file_123"],
  "graceMinutes": 1440
}

Validation:

  • storageKeys: Optional. Array of strings, at most 500 entries.
  • fileIds: Optional. Array of strings, at most 500 entries.
  • graceMinutes: Optional. Integer between 60 and 525600. Defaults to 1440.

At least one of storageKeys or fileIds must be non-empty.

The initial response is 202 with jobId and status. Poll the same endpoint with { "jobId": "..." }; a running job returns 202, and a completed job returns the result below.

Response:

{
  "graceMinutes": 1440,
  "storage": {
    "results": [
      { "key": "9f2c1b7a-3c5e-4f11-8a90-2b0d5c7e1a44", "deleted": true }
    ],
    "deleted": 1,
    "skipped": 0
  },
  "database": {
    "results": [
      {
        "id": "file_123",
        "deleted": false,
        "reason": "Stored object exists again"
      }
    ],
    "deleted": 0,
    "skipped": 1
  }
}

Skip reasons:

  • Storage keys: Not a valid storage key, A file now references this object, Object no longer exists, Object was written too recently
  • File IDs: Row no longer exists, Row does not map to a storage key, Row was created too recently, Stored object exists again

Error cases:

  • 400 Select at least one orphan to delete - Both arrays empty
  • 403 Only the first user can review orphaned files - Caller is not the first user
  • 503 Background worker queue is unavailable - The application could not queue the work
  • 422 Validation failed - Grace window out of range, array over 500 entries, or a non-string entry

On this page