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 (30,000 requests per 15 minutes per user). 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 (first user 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. String, between 8 and 128 characters and at most 72 bytes in UTF-8.
  • 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, a password that is too short or over 72 bytes, 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 (first user 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
}

Storage Bucket Configuration

GET /api/user/storage-status

Whether a bucket is connected, and whether the caller may connect one. Accessible to any authenticated user (used by the frontend for the setup banner).

Response:

{
  "configured": false,
  "canConfigure": true
}

GET /api/user/storage-config

Get the saved bucket settings (first user only). The secret access key is never returned, and the access key ID is masked.

Response:

{
  "configured": true,
  "version": 3,
  "provider": "s3",
  "endpoint": "https://s3.example.com",
  "region": "us-east-1",
  "bucket": "tma-files",
  "forcePathStyle": true,
  "accessKeyIdMasked": "AKIA••••MPLE",
  "updatedAt": "2026-10-09T09:53:35.671Z"
}

When no bucket is saved, the response is { "configured": false, "version": 0 }.

PUT /api/user/storage-config

Verify the settings against the live endpoint, then save them (first user only). Nothing is saved if a check fails.

Request Body:

{
  "provider": "s3",
  "endpoint": "https://s3.example.com",
  "region": "us-east-1",
  "bucket": "tma-files",
  "forcePathStyle": true,
  "accessKeyId": "AKIAIOSFODNN7EXAMPLE",
  "secretAccessKey": "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY",
  "expectedVersion": 3
}

Validation:

  • provider: Required. One of s3, r2, aws.
  • endpoint: Required for s3 and r2; optional for aws, where it defaults to https://s3.<region>.amazonaws.com. Must be an origin with no path, query, or credentials. http:// is accepted only for private network hosts.
  • region: Required for aws. Ignored for r2 (always auto). Defaults to us-east-1 for s3.
  • bucket: Required. 3-63 lowercase letters, numbers, dots, or hyphens.
  • forcePathStyle: Optional boolean, used only for s3 (default true).
  • accessKeyId, secretAccessKey: Required on first setup. Leave both empty to keep the saved keys. A new accessKeyId needs its secretAccessKey.
  • expectedVersion: Optional. The version the client last read; a different stored version returns 409.

Response:

The saved settings, in the same shape as GET /api/user/storage-config.

Errors:

  • 400: A setting is invalid. message names the field.
  • 409: The settings were changed by another session.
  • 422 with details: A field has the wrong type, for example a non-boolean forcePathStyle.
  • 422: A connection check failed. The body carries error: "STORAGE_PROBE_FAILED" and the failed step (connect, list, write, read, delete, or existing-files).
{
  "message": "Bucket \"tma-files\" was not found at this endpoint",
  "error": "STORAGE_PROBE_FAILED",
  "step": "connect"
}

POST /api/user/storage-config/test

Run the same checks as the PUT without saving anything (first user only). Takes the same request body.

Response:

{
  "ok": true,
  "checks": ["connect", "list", "write", "read", "delete", "existing-files"]
}

existing-files is listed only when files already exist. See Storage Bucket.

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,
  "desktopOnly": false,
  "canToggleDesktopOnly": false,
  "allowPasswordChange": false,
  "canToggleAllowPasswordChange": false
}

Response (first user / admin):

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

POST /api/user/signup-toggle

Enable or disable public user signup (first user 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 (first user 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 (first user 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 (first user only).

Response:

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

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

Update the share base URL configuration (first user 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 (first user 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 (first user 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 (first user 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.

Session and Access Time Configuration

GET /api/user/activity-config

Get the session timeout and last-opened tracking settings (first user only).

Response:

{
  "sessionIdleDays": 30,
  "accessTimeTracking": true,
  "accessTimeWindowMinutes": 60,
  "accessTimeFlushSeconds": 10
}

PUT /api/user/session-timeout-config

Set the session timeout (first user only).

Request Body:

{
  "idleDays": 14
}

Validation:

  • idleDays: Required. Whole number from 1 to 365.

Response: The full settings, as returned by GET /api/user/activity-config.

The change applies to existing sessions. A session idle longer than a new, shorter timeout is refused on its next request.

PUT /api/user/access-time-config

Set last-opened tracking (first user only).

Request Body:

{
  "enabled": true,
  "windowMinutes": 60,
  "flushSeconds": 10
}

Validation:

  • enabled: Required. Boolean.
  • windowMinutes: Required. Whole number from 0 to 1440.
  • flushSeconds: Required. Whole number from 1 to 300.

Response: The full settings, as returned by GET /api/user/activity-config.

Neither change needs a restart: the process that handles the request applies it at once, and every other backend process within 15 seconds.

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 (first user 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/desktop-only-config

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

Response:

{
  "desktopOnly": false
}

PUT /api/user/desktop-only-config

Update the desktop-only access setting (first user only). When enabled, the backend blocks browser access to the main app. Share links, /health, /metrics, and the OnlyOffice file and callback endpoints (called by the Document Server) remain available.

Request Body:

{
  "enabled": true
}

Validation:

  • enabled: Required. Must be a boolean.

Response:

{
  "desktopOnly": true
}

Cloud Drive Configuration

GET /api/user/cloud-drive-config

Get the Cloud Drive access mode. Accessible to any authenticated user; the desktop app reads it to decide what the mounted drive allows.

Response:

{
  "saveOnly": true,
  "canConfigure": false
}
  • saveOnly: When true, other apps can browse and save into the drive but cannot read file contents from it. True unless the first user turned it off.
  • canConfigure: True for the first user.

PUT /api/user/cloud-drive-config

Update the Cloud Drive access mode (first user only). Mounted drives apply the change within a minute.

Request Body:

{
  "saveOnly": false
}

Validation:

  • saveOnly: Required. Must be a boolean.

Response:

{
  "saveOnly": false,
  "canConfigure": 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 (first user only). When enabled, users can change their password from Settings → Security.

Request Body:

{
  "enabled": true
}

Validation:

  • enabled: Required. Must be a boolean.

Response:

{
  "allowPasswordChange": true
}

Active Desktop Clients

GET /api/user/active-clients

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

Response:

{
  "clients": [
    {
      "id": "user_123:client_abc",
      "userId": "user_123",
      "userName": "User Name",
      "userEmail": "user@example.com",
      "appVersion": "X.Y.Z",
      "platform": "windows",
      "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 a desktop app heartbeat (authenticated users). The desktop app sends one every minute.

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": "windows",
  "clientId": "0ca7c806-8ee8-4fde-80e1-f2f43e5f9bfa"
}

Validation:

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

The heartbeat is linked to the session in the signed-in token. A sessionId in the request body is ignored, so a client cannot record presence for a session that is not its own.

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

Last updated on

On this page

List UsersGET /api/user/allSub-usersGET /api/user/sub-usersPOST /api/user/sub-usersPUT /api/user/sub-users/:idDELETE /api/user/sub-users/:idStorageGET /api/user/storagePUT /api/user/storage-limitStorage Bucket ConfigurationGET /api/user/storage-statusGET /api/user/storage-configPUT /api/user/storage-configPOST /api/user/storage-config/testSignup StatusGET /api/signup-statusGET /api/user/signup-statusPOST /api/user/signup-toggleOnlyOffice ConfigurationGET /api/user/onlyoffice-configuredGET /api/user/onlyoffice-configPUT /api/user/onlyoffice-configShare Base URL ConfigurationGET /api/user/share-base-url-configPUT /api/user/share-base-url-configMax Upload Size ConfigurationGET /api/user/max-upload-size-configPUT /api/user/max-upload-size-configKnown Proxies ConfigurationGET /api/user/known-proxies-configPUT /api/user/known-proxies-configSession and Access Time ConfigurationGET /api/user/activity-configPUT /api/user/session-timeout-configPUT /api/user/access-time-configHide File Extensions ConfigurationGET /api/user/hide-file-extensions-configPUT /api/user/hide-file-extensions-configDesktop App Only Access ConfigurationGET /api/user/desktop-only-configPUT /api/user/desktop-only-configCloud Drive ConfigurationGET /api/user/cloud-drive-configPUT /api/user/cloud-drive-configPassword Change ConfigurationGET /api/user/password-change-configPUT /api/user/password-change-configActive Desktop ClientsGET /api/user/active-clientsPOST /api/user/client-heartbeatOrphaned FilesGET /api/user/orphansPOST /api/user/orphans/deleteRelated Topics