Skip to content
SENS Platform Docs

Core Administration API

All core-administration routes require a current Bearer session without a pending password change. Request bodies and query strings reject unknown fields. Lists use opaque cursor pagination. Route-specific limits are part of the OpenAPI contract.

Tenant routes are available under /v1/tenants. Platform administrators can create, disable, reactivate, and change every tenant field. A tenant administrator can change only the name and complete metadata map of their own active tenant. Other tenant roles can read only tenants granted to them.

GET /v1/tenants is a counted cursor collection. Its default page size is 25, and it accepts only 25, 50, or 100. The response’s totalCount is the exact count for the same search, status filter, and authorization scope as items. Both values come from one database statement and snapshot. Results use stable ascending createdAt, tenantId order. The opaque cursor is bound to the current actor, grants, filters, and page size; malformed, stale, oversized, or incompatible cursors return 400 VALIDATION_FAILED.

Every Tenant response contains the internal UUID tenantId and the public integer tenantNumber. Tenant selection uses the canonical number, for example GET /v1/tenants/17 and GET /v1/tenants/17/organization-units. Existing organization units and memberships are addressed by their globally unique aggregate IDs at /v1/organization-units/{organizationUnitId} and /v1/memberships/{membershipId}. Removed UUID and tenant-nested member paths have no aliases.

Organization units form a flexible tree below a tenant. Platform and tenant administrators see the full tree. Organization administrators and viewers see their active scope plus read-only ancestors needed for navigation. The scopeRelation response field distinguishes accessible from ancestor. The existing GET /v1/tenants/{tenantNumber}/organization-units collection remains a flat, searchable list of every visible unit, ordered by createdAt, organizationUnitId. The API defaults to 50 rows; the Web client sends an explicit limit of 100 for this compatibility collection. Its cursor is bound to the actor, current platform roles and memberships, tenant, normalized search, status filter, and page size. A changed input or missing cursor position returns 400 VALIDATION_FAILED. Tree navigation uses three separate cursor collections:

  • GET /v1/tenants/{tenantNumber}/organization-units/roots returns only visible roots.
  • GET /v1/organization-units/{organizationUnitId}/children returns only the direct visible children of that unit.
  • GET /v1/tenants/{tenantNumber}/organization-units/search?q=... searches keys and names throughout the complete actor-visible hierarchy. Every match includes visibleAncestorPath, ordered from the visible root to its parent.

Every organization response contains hasChildren, calculated for the caller’s visible hierarchy. Tree pages are ordered by key, organizationUnitId. Their opaque cursors are bound to the actor, current memberships, tenant, navigation mode, parent or search term, status filter, and page size. A cursor cannot be reused after one of those inputs changes; malformed, stale, or incompatible values return 400 VALIDATION_FAILED.

Setting an organization unit to disabled makes its entire subtree effectively disabled without rewriting child status values. Only platform and tenant administrators can reactivate disabled subtrees. Reparenting is serialized per tenant, rejects cycles and cross-tenant parents, and requires an organization administrator to retain administration over the complete subtree before and after the move. Organization administrators cannot move a unit to the root.

Global /v1/users routes are restricted to platform administrators. User creation requires a temporary password, stores only an Argon2id hash, and sets passwordChangeRequired to true. Password values are never returned. Resetting a password revokes all sessions and cannot be used on the calling administrator itself.

Tenant user views expose users with membership history in that tenant. They do not expose global roles, protection state, or global metadata.

GET /v1/users, GET /v1/tenants/{tenantNumber}/users, and GET /v1/tenants/{tenantNumber}/memberships are counted cursor collections. They default to 25 rows and accept only 25, 50, or 100. totalCount is exact for the same filters and authorization scope as items, calculated in the same database statement and snapshot. User pages use stable ascending createdAt, userId order; membership pages use createdAt, membershipId. Their opaque cursors are bound to the actor, current grants, tenant where applicable, filters, and page size. Malformed, stale, oversized, or incompatible cursors return 400 VALIDATION_FAILED.

Membership list items include the authorized User display name and email plus the Organization Unit name, or null for tenant-wide grants. These labels are part of the list projection and come from the same authorization-scoped query; clients do not need per-row detail requests. Global roles, User metadata, and Organization data outside the addressed Tenant are not included.

Memberships are independent grants. tenant_admin is tenant-wide, org_admin requires an organization unit, and viewer may be tenant-wide or organization-specific. Only a platform administrator can introduce a user with no prior membership in a tenant. Tenant administrators can manage all current and historical grants for known tenant users.

Platform role changes use /v1/users/{userId}/platform-roles/platform_admin. An administrator cannot revoke their own role or disable their own account, and the last active platform administrator cannot be removed. Accounts actually created by auth:admin are permanent break-glass accounts: their profile and password remain editable, but they cannot be disabled, deleted, or stripped of the platform role.

POST /v1/tenants/{tenantNumber}/imports/iota-snapshot is a temporary, platform-administrator-only migration operation. It accepts an allowed IOTA HTTPS base URL plus username and password, loads every IOTA tenant visible to that login, and creates the valid device types and devices in the active target tenant. The request rejects query parameters and all unknown body fields.

The credentials exist only for the current request. They are not configuration, are not persisted, and do not appear in logs, audit metadata, errors, or the response. The allowed URL has the exact form https://commonapi-<slug>.xaas-a0a0.cloud/api; redirects, credentials in the URL, alternate ports, query strings, and fragments are rejected.

The operation is create-only. Existing records are never updated, reactivated, deleted, or moved between tenants. A valid source subset commits atomically with per-entity and summary audit events. Expected source conflicts appear in the bounded issues response; IOTA authentication, timeout, and response failures return a safe DEPENDENCY_FAILURE. See the IOTA snapshot import contract for mapping, limits, and removal requirements. Human-readable IOTA type codes remain names; incompatible technical keys are derived deterministically, and identical codes across source tenants merge. IOTA asset types, asset pseudo-devices, and devices directly connected to an asset are intentionally excluded and reported as asset_excluded.

Every DELETE in this API is a soft delete that sets status to disabled and preserves identifiers and history. Reactivation uses PATCH with status: "active". Expected duplicates, hierarchy cycles, and protection violations return 409 CONFLICT; hidden or cross-tenant resources return 404 RESOURCE_NOT_FOUND.

See the OpenAPI reference for exact schemas and route parameters.