diff --git a/intro/changelog.md b/intro/changelog.md
index a4a0a63..71f78b4 100644
--- a/intro/changelog.md
+++ b/intro/changelog.md
@@ -14,6 +14,16 @@ slug: changelog
Listed below are all the changes to the SeaTable API. Each date corresponds to a new version of SeaTable Server Enterprise Edition. If you’re looking for changes beyond the API, see the SeaTable [Changelog](https://seatable.com/changelog) or check out the [SeaTable Blog](https://seatable.com/blog) for detailed release notes.
+## Version 7.0
+
+> 📘 New requests
+>
+> **Account Operations - Team Admin**
+>
+> - [List Managed Apps](/reference/listmanagedapps) get
+> - [List Usable Apps](/reference/listusableapps) get
+> - [List Bases Shared to User](/reference/listbasessharedtouser-1) get
+
## Version 6.2 (21.07.2026)
> 🚧 Breaking changes
diff --git a/team_admin_account_operations.yaml b/team_admin_account_operations.yaml
index d5346c7..ac79657 100644
--- a/team_admin_account_operations.yaml
+++ b/team_admin_account_operations.yaml
@@ -451,6 +451,7 @@ paths:
type: string
last_login:
type: string
+ nullable: true
self_usage:
type: integer
quota:
@@ -715,6 +716,88 @@ paths:
example:
success: true
+ /api/v2.1/org/{org_id}/admin/users/{user_id}/managed-external-apps/:
+ get:
+ tags:
+ - Users
+ summary: List Managed Apps
+ operationId: listManagedApps
+ description: >-
+ List all the universal apps a certain user manages, i.e. the apps in
+ which the user has the `admin` role. Inactive apps and apps of deleted
+ bases are not included.
+ security:
+ - AccountTokenAuth: []
+ parameters:
+ - $ref: "#/components/parameters/org_id"
+ - $ref: "#/components/parameters/user_id"
+ - $ref: "#/components/parameters/page"
+ - $ref: "#/components/parameters/per_page"
+ responses:
+ "200":
+ description: OK
+ content:
+ application/json:
+ schema:
+ type: object
+ example:
+ managed_apps:
+ - app_user_id: 12
+ app_id: 4
+ app_name: Customer Portal
+ app_uuid: 25ed3722-7988-4389-8095-9a46373cd8a5
+ app_config: '{"app_type": "universal-app", "app_name": "Customer Portal", "settings": {"pages": [], "navigation": []}}'
+ role_name: admin
+ permission: rw
+ dtable_uuid: c56624fd757f4f4bae7de76235aeb1f0
+ dtable_name: CRM
+ workspace_id: 2
+ link: https://cloud.seatable.io/external-apps/25ed3722-7988-4389-8095-9a46373cd8a5/
+ edit_link: https://cloud.seatable.io/dtable/external-apps-edit/25ed3722-7988-4389-8095-9a46373cd8a5/
+ joined_at: '2025-11-28T14:16:30+01:00'
+ count: 1
+
+ /api/v2.1/org/{org_id}/admin/users/{user_id}/can-use-external-apps/:
+ get:
+ tags:
+ - Users
+ summary: List Usable Apps
+ operationId: listUsableApps
+ description: >-
+ List all the universal apps a certain user can use, i.e. the apps in
+ which the user has a role other than `admin`. Inactive apps and apps
+ of deleted bases are not included.
+ security:
+ - AccountTokenAuth: []
+ parameters:
+ - $ref: "#/components/parameters/org_id"
+ - $ref: "#/components/parameters/user_id"
+ - $ref: "#/components/parameters/page"
+ - $ref: "#/components/parameters/per_page"
+ responses:
+ "200":
+ description: OK
+ content:
+ application/json:
+ schema:
+ type: object
+ example:
+ can_use_apps:
+ - app_user_id: 13
+ app_id: 4
+ app_name: Customer Portal
+ app_uuid: 25ed3722-7988-4389-8095-9a46373cd8a5
+ app_config: '{"app_type": "universal-app", "app_name": "Customer Portal", "settings": {"pages": [], "navigation": []}}'
+ role_name: default
+ permission: rw
+ dtable_uuid: c56624fd757f4f4bae7de76235aeb1f0
+ dtable_name: CRM
+ workspace_id: 2
+ link: https://cloud.seatable.io/external-apps/25ed3722-7988-4389-8095-9a46373cd8a5/
+ edit_link: https://cloud.seatable.io/dtable/external-apps-edit/25ed3722-7988-4389-8095-9a46373cd8a5/
+ joined_at: '2025-11-28T14:16:30+01:00'
+ count: 1
+
# Bases
/api/v2.1/org/{org_id}/admin/dtables/:
get:
@@ -837,6 +920,48 @@ paths:
owner_deleted: false
rows_count: 0
count: 2
+ /api/v2.1/org/{org_id}/admin/users/{user_id}/shared-dtables/:
+ get:
+ tags:
+ - Bases
+ summary: List Bases Shared to User
+ operationId: listBasesSharedToUser
+ description: List all the bases shared to a certain user by the user's ID.
+ security:
+ - AccountTokenAuth: []
+ parameters:
+ - $ref: "#/components/parameters/org_id"
+ - $ref: "#/components/parameters/user_id"
+ - $ref: "#/components/parameters/page"
+ - $ref: "#/components/parameters/per_page"
+ responses:
+ "200":
+ description: OK
+ content:
+ application/json:
+ schema:
+ type: object
+ example:
+ dtable_list:
+ - id: 8
+ workspace_id: 2
+ uuid: 3a6e34b7-c9e1-4c2f-8b0e-2f5d3f21c7a4
+ name: CRM
+ created_at: '2025-11-28T14:16:30+01:00'
+ updated_at: '2025-11-28T14:17:04+01:00'
+ color: "#E91E63"
+ text_color: null
+ icon: icon-user-interview
+ is_encrypted: false
+ in_storage: true
+ backend: js
+ creator: org-1-admin
+ modifier: org-1-admin
+ file_size: 1389
+ rows_count: 7
+ from_user: e38487b4357e40c8b1359223ba87a1af@auth.local
+ from_user_name: org-1-admin
+ count: 1
/api/v2.1/org/{org_id}/admin/dtables/{base_uuid}/:
get:
tags:
diff --git a/tests/__snapshots__/test_team_apps/test_listManagedApps.json b/tests/__snapshots__/test_team_apps/test_listManagedApps.json
new file mode 100644
index 0000000..b10455e
--- /dev/null
+++ b/tests/__snapshots__/test_team_apps/test_listManagedApps.json
@@ -0,0 +1,20 @@
+{
+ "count": 1,
+ "managed_apps": [
+ {
+ "app_config": "{\"app_type\": \"universal-app\", \"app_name\": \"Test App\", \"settings\": {\"pages\": [], \"navigation\": []}}",
+ "app_id": "int",
+ "app_name": "Test App",
+ "app_user_id": "int",
+ "app_uuid": "str",
+ "dtable_name": "ManagedAppsTest",
+ "dtable_uuid": "str",
+ "edit_link": "str",
+ "joined_at": "str",
+ "link": "str",
+ "permission": "rw",
+ "role_name": "admin",
+ "workspace_id": "int"
+ }
+ ]
+}
diff --git a/tests/__snapshots__/test_team_apps/test_listUsableApps.json b/tests/__snapshots__/test_team_apps/test_listUsableApps.json
new file mode 100644
index 0000000..0e994ff
--- /dev/null
+++ b/tests/__snapshots__/test_team_apps/test_listUsableApps.json
@@ -0,0 +1,20 @@
+{
+ "can_use_apps": [
+ {
+ "app_config": "{\"app_type\": \"universal-app\", \"app_name\": \"Test App\", \"settings\": {\"pages\": [], \"navigation\": []}}",
+ "app_id": "int",
+ "app_name": "Test App",
+ "app_user_id": "int",
+ "app_uuid": "str",
+ "dtable_name": "UsableAppsTest",
+ "dtable_uuid": "str",
+ "edit_link": "str",
+ "joined_at": "str",
+ "link": "str",
+ "permission": "rw",
+ "role_name": "default",
+ "workspace_id": "int"
+ }
+ ],
+ "count": 1
+}
diff --git a/tests/__snapshots__/test_team_bases/test_listBasesSharedToUser.json b/tests/__snapshots__/test_team_bases/test_listBasesSharedToUser.json
new file mode 100644
index 0000000..133785c
--- /dev/null
+++ b/tests/__snapshots__/test_team_bases/test_listBasesSharedToUser.json
@@ -0,0 +1,25 @@
+{
+ "count": 1,
+ "dtable_list": [
+ {
+ "backend": "js",
+ "color": null,
+ "created_at": "str",
+ "creator": "str",
+ "file_size": "int",
+ "from_user": "str",
+ "from_user_name": "str",
+ "icon": null,
+ "id": "int",
+ "in_storage": true,
+ "is_encrypted": false,
+ "modifier": "str",
+ "name": "SharedBasesTest",
+ "rows_count": 0,
+ "text_color": null,
+ "updated_at": "str",
+ "uuid": "str",
+ "workspace_id": "int"
+ }
+ ]
+}
diff --git a/tests/test_team_apps.py b/tests/test_team_apps.py
new file mode 100644
index 0000000..2e9764a
--- /dev/null
+++ b/tests/test_team_apps.py
@@ -0,0 +1,161 @@
+import json
+import pytest
+import requests
+from conftest import (
+ BASE_URL, Secret, TeamAdmin, system_admin_account_operations, team_admin_account_operations,
+ user_account_operations,
+)
+from schemathesis import Case
+from syrupy.assertion import SnapshotAssertion
+from syrupy.matchers import path_type
+
+pytestmark = pytest.mark.needs_large_license
+
+
+def test_listManagedApps(team: TeamAdmin, system_admin_account_token: Secret, snapshot_json: SnapshotAssertion):
+ """Create a universal app, make the team admin its admin and verify it appears in listManagedApps."""
+ headers = {'Authorization': f'Bearer {team.account_token}'}
+ path_parameters = {'org_id': team.team_id}
+
+ # Custom app roles require the "advanced customization" permission, which the default team role lacks
+ case: Case = system_admin_account_operations.find_operation_by_id('updateTeam') \
+ .Case(path_parameters=path_parameters, body={'role': 'org_enterprise'})
+ response = case.call(headers={'Authorization': f'Bearer {system_admin_account_token.value}'})
+ assert response.status_code == 200
+
+ # The team admin will be the app's admin, so get their own user ID
+ case: Case = user_account_operations.find_operation_by_id('getAccountInfo').Case()
+ response = case.call(headers=headers)
+ assert response.status_code == 200
+ user_id = response.json()['email']
+
+ # Create a base in the team admin's personal workspace
+ case: Case = user_account_operations.find_operation_by_id('listWorkspaces').Case()
+ response = case.call(headers=headers)
+ assert response.status_code == 200
+ ws_id = next(w for w in response.json()['workspace_list'] if w.get('type') == 'personal')['id']
+
+ body = {'workspace_id': ws_id, 'name': 'ManagedAppsTest'}
+ case: Case = user_account_operations.find_operation_by_id('createBase').Case(body=body)
+ response = case.call(headers=headers)
+ assert response.status_code == 201
+ base_uuid = response.json()['table']['uuid']
+
+ # Create a universal app, an "admin" role and add the team admin with that role (endpoints not in the spec)
+ app_config = json.dumps({'app_type': 'universal-app', 'app_name': 'Test App', 'settings': {'pages': [], 'navigation': []}})
+ response = requests.post(
+ f'{BASE_URL}/api/v2.1/workspace/{ws_id}/dtable/ManagedAppsTest/external-apps/',
+ headers=headers, data={'app_type': 'universal-app', 'app_config': app_config},
+ )
+ assert response.status_code == 201
+ app_uuid = response.json()['external_app']['app_uuid']
+
+ response = requests.post(
+ f'{BASE_URL}/api/v2.1/universal-apps/{app_uuid}/app-roles/',
+ headers=headers, data={'role_name': 'admin', 'permission': 'rw'},
+ )
+ assert response.status_code == 200
+ role_id = response.json()['app_role']['id']
+
+ response = requests.post(
+ f'{BASE_URL}/api/v2.1/universal-apps/{app_uuid}/app-users/',
+ headers=headers, data={'app_user': user_id, 'app_role_id': role_id},
+ )
+ assert response.status_code == 200
+
+ # List the user's managed apps
+ case: Case = team_admin_account_operations.find_operation_by_id('listManagedApps') \
+ .Case(path_parameters={'org_id': team.team_id, 'user_id': user_id})
+ response = case.call(headers=headers)
+
+ assert response.status_code == 200
+
+ data = response.json()
+ assert data['count'] == 1
+ assert len(data['managed_apps']) == 1
+
+ managed_app = data['managed_apps'][0]
+ assert managed_app['app_uuid'] == app_uuid
+ assert managed_app['dtable_uuid'] == base_uuid.replace('-', '')
+
+ matcher = path_type({
+ r"managed_apps\.\d+\.app_user_id": (int,),
+ r"managed_apps\.\d+\.app_id": (int,),
+ r"managed_apps\.\d+\.app_uuid": (str,),
+ r"managed_apps\.\d+\.dtable_uuid": (str,),
+ r"managed_apps\.\d+\.workspace_id": (int,),
+ r"managed_apps\.\d+\.link": (str,),
+ r"managed_apps\.\d+\.edit_link": (str,),
+ r"managed_apps\.\d+\.joined_at": (str,),
+ }, regex=True)
+
+ assert snapshot_json(matcher=matcher) == data
+
+
+def test_listUsableApps(team: TeamAdmin, snapshot_json: SnapshotAssertion):
+ """Create a universal app, add the team admin with the default role and verify it appears in listUsableApps."""
+ headers = {'Authorization': f'Bearer {team.account_token}'}
+
+ case: Case = user_account_operations.find_operation_by_id('getAccountInfo').Case()
+ response = case.call(headers=headers)
+ assert response.status_code == 200
+ user_id = response.json()['email']
+
+ # Create a base in the team admin's personal workspace
+ case: Case = user_account_operations.find_operation_by_id('listWorkspaces').Case()
+ response = case.call(headers=headers)
+ assert response.status_code == 200
+ ws_id = next(w for w in response.json()['workspace_list'] if w.get('type') == 'personal')['id']
+
+ body = {'workspace_id': ws_id, 'name': 'UsableAppsTest'}
+ case: Case = user_account_operations.find_operation_by_id('createBase').Case(body=body)
+ response = case.call(headers=headers)
+ assert response.status_code == 201
+ base_uuid = response.json()['table']['uuid']
+
+ # Create a universal app and add the team admin with its auto-created "default" role (endpoints not in the spec)
+ app_config = json.dumps({'app_type': 'universal-app', 'app_name': 'Test App', 'settings': {'pages': [], 'navigation': []}})
+ response = requests.post(
+ f'{BASE_URL}/api/v2.1/workspace/{ws_id}/dtable/UsableAppsTest/external-apps/',
+ headers=headers, data={'app_type': 'universal-app', 'app_config': app_config},
+ )
+ assert response.status_code == 201
+ app_uuid = response.json()['external_app']['app_uuid']
+
+ response = requests.get(f'{BASE_URL}/api/v2.1/universal-apps/{app_uuid}/app-roles/', headers=headers)
+ assert response.status_code == 200
+ role_id = next(r['id'] for r in response.json()['app_roles'] if r['role_name'] == 'default')
+
+ response = requests.post(
+ f'{BASE_URL}/api/v2.1/universal-apps/{app_uuid}/app-users/',
+ headers=headers, data={'app_user': user_id, 'app_role_id': role_id},
+ )
+ assert response.status_code == 200
+
+ # List the apps the user can use
+ case: Case = team_admin_account_operations.find_operation_by_id('listUsableApps') \
+ .Case(path_parameters={'org_id': team.team_id, 'user_id': user_id})
+ response = case.call(headers=headers)
+
+ assert response.status_code == 200
+
+ data = response.json()
+ assert data['count'] == 1
+ assert len(data['can_use_apps']) == 1
+
+ usable_app = data['can_use_apps'][0]
+ assert usable_app['app_uuid'] == app_uuid
+ assert usable_app['dtable_uuid'] == base_uuid.replace('-', '')
+
+ matcher = path_type({
+ r"can_use_apps\.\d+\.app_user_id": (int,),
+ r"can_use_apps\.\d+\.app_id": (int,),
+ r"can_use_apps\.\d+\.app_uuid": (str,),
+ r"can_use_apps\.\d+\.dtable_uuid": (str,),
+ r"can_use_apps\.\d+\.workspace_id": (int,),
+ r"can_use_apps\.\d+\.link": (str,),
+ r"can_use_apps\.\d+\.edit_link": (str,),
+ r"can_use_apps\.\d+\.joined_at": (str,),
+ }, regex=True)
+
+ assert snapshot_json(matcher=matcher) == data
diff --git a/tests/test_team_bases.py b/tests/test_team_bases.py
new file mode 100644
index 0000000..52545cd
--- /dev/null
+++ b/tests/test_team_bases.py
@@ -0,0 +1,84 @@
+import pytest
+from conftest import (
+ TeamAdmin, team_admin_account_operations, user_account_operations, generate_password,
+)
+from random import randint
+from schemathesis import Case
+from syrupy.assertion import SnapshotAssertion
+from syrupy.matchers import path_type
+
+pytestmark = pytest.mark.needs_large_license
+
+
+def test_listBasesSharedToUser(team: TeamAdmin, snapshot_json: SnapshotAssertion):
+ """Share a base from the team admin to a team member and verify it appears in listBasesSharedToUser."""
+ headers = {'Authorization': f'Bearer {team.account_token}'}
+ path_parameters = {'org_id': team.team_id}
+
+ # Add a user to the team (emails are reserved forever, so use a unique one per run)
+ body = {
+ 'email': f'shared-bases-test-{randint(1, 1000000)}@example.com',
+ 'name': 'Shared Bases Test',
+ 'password': generate_password(),
+ 'with_workspace': True,
+ }
+ case: Case = team_admin_account_operations.find_operation_by_id('addUser') \
+ .Case(path_parameters=path_parameters, body=body)
+ response = case.call(headers=headers)
+ assert response.status_code == 200
+ user_id = response.json()['email']
+
+ # Get the team admin's own user ID and personal workspace
+ case: Case = user_account_operations.find_operation_by_id('getAccountInfo').Case()
+ response = case.call(headers=headers)
+ assert response.status_code == 200
+ admin_user_id = response.json()['email']
+
+ case: Case = user_account_operations.find_operation_by_id('listWorkspaces').Case()
+ response = case.call(headers=headers)
+ assert response.status_code == 200
+ ws_id = next(w for w in response.json()['workspace_list'] if w.get('type') == 'personal')['id']
+
+ body = {'workspace_id': ws_id, 'name': 'SharedBasesTest'}
+ case: Case = user_account_operations.find_operation_by_id('createBase').Case(body=body)
+ response = case.call(headers=headers)
+ assert response.status_code == 201
+ base_uuid = response.json()['table']['uuid']
+
+ # Share the base to the new user
+ body = {'email': user_id, 'permission': 'r'}
+ case: Case = user_account_operations.find_operation_by_id('createUserShare') \
+ .Case(path_parameters={'workspace_id': ws_id, 'base_name': 'SharedBasesTest'}, body=body)
+ response = case.call(headers=headers)
+ assert response.status_code == 201
+
+ # List bases shared to the user
+ case: Case = team_admin_account_operations.find_operation_by_id('listBasesSharedToUser') \
+ .Case(path_parameters={'org_id': team.team_id, 'user_id': user_id})
+ response = case.call(headers=headers)
+
+ assert response.status_code == 200
+
+ data = response.json()
+ assert data['count'] == 1
+ assert len(data['dtable_list']) == 1
+
+ shared_base = data['dtable_list'][0]
+ assert shared_base['uuid'] == base_uuid
+ assert shared_base['from_user'] == admin_user_id
+
+ matcher = path_type({
+ r"dtable_list\.\d+\.id": (int,),
+ r"dtable_list\.\d+\.workspace_id": (int,),
+ r"dtable_list\.\d+\.uuid": (str,),
+ r"dtable_list\.\d+\.created_at": (str,),
+ r"dtable_list\.\d+\.updated_at": (str,),
+ r"dtable_list\.\d+\.creator": (str,),
+ r"dtable_list\.\d+\.modifier": (str,),
+ r"dtable_list\.\d+\.file_size": (int,),
+ r"dtable_list\.\d+\.from_user": (str,),
+ r"dtable_list\.\d+\.from_user_name": (str,),
+ }, regex=True)
+
+ assert snapshot_json(matcher=matcher) == data
+