Skip to main content

Principals

A Principal is the unified id space over Users and Teams: anything that can own or be shared content (a User, a Team, or the synthetic "Everyone") is referenced by one stable Principal id. A User principal's id is the username; a Team principal's id is the Team uuid; the "Everyone" principal is the synthetic id *.

The Principal table is maintained entirely by database triggers, never by application code. A row is created on every User insert (id and userId set to the username) and on every Team insert (id and teamId set to the Team uuid), and is removed when the underlying User or Team is deleted. You therefore never create or delete Principal rows through this API; you list them, resolve one to its underlying resource, and reassign the content a Principal owns.

There are three routes:

  • GET /api/principal-list
  • GET /api/principals/{id}
  • POST /api/principals/{id}/_reassign

No license feature gates any Principal route.

GET /api/principal-list

List every User and Team principal in the tenant, one display-ready row each.

Authentication: Required (open within the tenant, no permission check)

This route is intentionally open to any authenticated caller because content can be shared to, or commented about, any Principal, so any caller may need to enumerate them. Its ETag combines the User and Team table ETags.

Response:

A plain JSON array, not a HAL collection envelope and not a { total, limit, offset, results } paged envelope. The handler runs a single SQL join (principal to User or Team to avatar) and maps each row through mapSharePrincipal. Rows are sorted case-insensitively by team.name || user.displayName || user.username ascending, with nulls last. This ordering is load-bearing: the Informer GO principal-search UI renders only the first rows, so it must be stable run to run.

Each row carries:

FieldTypeDescription
idstringThe Principal id (username for a User, Team uuid for a Team, * for Everyone)
userIdstring | nullThe username when the row is a User, else null
teamIdstring | nullThe Team uuid when the row is a Team, else null
typestringUser, Team, or Everyone
namestringDisplay label, resolved as team.name || displayName || username
usernamestring | nullUsername (User rows only)
displayNamestring | nullDisplay name (User rows only)
emailstring | nullEmail (User rows only)
initialsstringAvatar initials, stamped by addAvatarData (User rows)
avatarColorobjectThemed avatar disc colors (background, text), stamped by addAvatarData (User rows)
colorIndexnumberIndex into the avatar color palette (User rows)
avatarUrlstringUploaded avatar URL, present only when the User has uploaded an avatar
colorstring | nullTeam color (Team rows)
iconstring | nullTeam icon (Team rows)
materialIconstring | nullTeam Material icon name (Team rows)
Avatar colors are derived

colorIndex and avatarColor are derived by hashing the tenant slug, so the captured values reflect the per-run test tenant rather than any fixed palette position.

List principals (users + teams)GET /api/principal-list
GET /api/principal-list returns a PLAIN ARRAY (not a HAL collection or paged envelope) of every user and team principal in the tenant, one display-ready row each, sorted case-insensitively by team.name || user.displayName || user.username ASC NULLS LAST. User rows carry type:"User" plus username/displayName/email and avatar fields (initials, avatarColor, colorIndex; avatarUrl only when an avatar was uploaded); team rows carry type:"Team" plus name/color/icon/materialIcon. The synthetic "Everyone" principal is NOT included. The route is open within the tenant (no permission check) because content can be shared to / commented about any principal.
Response · 200
[
{
"userId": "annie.larson",
"teamId": null,
"id": "annie.larson",
"username": "annie.larson",
"displayName": "Annie Larson",
"email": "annie.larson@example.com",
"avatarUpdatedAt": null,
"name": "Annie Larson",
"color": null,
"icon": null,
"materialIcon": null,
"type": "User",
"avatarColor": {
"background": "#81C784",
"text": "#1B5E20"
},
"colorIndex": 4,
"initials": "AL"
},
{
"userId": "casey.morgan",
"teamId": null,
"id": "casey.morgan",
"username": "casey.morgan",
"displayName": "Casey Morgan",
"email": "casey.morgan@example.com",
"avatarUpdatedAt": null,
"name": "Casey Morgan",
"color": null,
"icon": null,
"materialIcon": null,
"type": "User",
"avatarColor": {
"background": "#81C784",
"text": "#1B5E20"
},
"colorIndex": 4,
"initials": "CM"
},
{
"userId": "admin",
"teamId": null,
"id": "admin",
"username": "admin",
"displayName": "acme Administrator",
"email": null,
"avatarUpdatedAt": null,
"name": "acme Administrator",
"color": null,
"icon": null,
"materialIcon": null,
"type": "User",
"avatarColor": {
"background": "#81C784",
"text": "#1B5E20"
},
"colorIndex": 4,
"initials": "EA"
},
"… 2 more items (5 total) — omitted from docs"
]
Captured from the API examples test suite; ids and timestamps are normalized.

GET /api/principals/{id}

Resolve a Principal id to its underlying resource.

Authentication: Required (open within the tenant, no permission check)

Path Parameters:

ParameterTypeDescription
idstringThe Principal id (a username for a User, a Team uuid for a Team)

Response:

This route never returns a Principal body of its own. It resolves the Principal row and then issues a 302 redirect: a User principal redirects to /api/users/{username} and a Team principal redirects to /api/teams/{id}. The redirect target rides in the Location header, so the response body is empty. Use this to resolve "is this id a User or a Team, and where is its resource?" without branching client-side. A missing id answers 404.

Look up a user principalGET /api/principals/admin
GET /api/principals/{id} normalizes a principal id then 302-REDIRECTS: for a user principal (id = username, e.g. "admin") it redirects to user.lookup (/api/users/{username}). It never returns a principal body of its own; the redirect target rides in the Location header, so the captured body is empty. Use this to resolve "is this id a user or a team, and where is its resource?" without branching client-side.
Response · 302
""
Captured from the API examples test suite; ids and timestamps are normalized.

The Team case behaves identically, redirecting to the Team resource instead:

Look up a team principalGET /api/principals/order-desk
GET /api/principals/{id} for a team principal (id = the team uuid) 302-redirects to team.lookup (/api/teams/{id}). Same indirection as the user case: no body, the redirect target is in the Location header.
Response · 302
""
Captured from the API examples test suite; ids and timestamps are normalized.

POST /api/principals/{id}/_reassign

Transfer every entity owned by a Principal to another Principal.

Authentication: reassign permission on the source Principal

The caller must hold the reassign permission on the source {id} Principal: they must be a superuser, the User themself (for a User principal), or a Team admin (for a Team principal). When the caller lacks reassign, the route answers 401.

Path Parameters:

ParameterTypeDescription
idstringThe source Principal id whose owned content is moved

Request Body:

FieldTypeRequiredDescription
tostringYesThe destination Principal id that receives the content

Example Request:

{ "to": "admin" }

This route moves every entity owned by the {id} Principal to the to Principal, across all ten owned-entity models: Dataset, Datasource, Report, Query, File, Job, Visual, Assistant, Library, and Template. Each moved entity has its ownerId set to to.

The lifecycle runs inside a route transaction. A pre-block first destroys any in-progress draft rows owned by {id} (Dataset, Report, or Query rows with editingId set) so half-edited drafts are not transferred, then loads the remaining owned entities and the source Principal's underlying User or Team.

Drafts are destroyed, not transferred

In-progress drafts owned by the source Principal are deleted before the transfer runs. They do not move to the destination Principal.

Response:

Responds 200 with the array of reassigned entities, HAL-wrapped under inf:reassign. An empty source Principal yields a 200 with an empty collection.

Reassign a principal’s owned contentPOST /api/principals/casey.morgan/_reassign
POST /api/principals/{id}/_reassign moves EVERY entity owned by the {id} principal to the `to` principal, across all ten owned-entity models (Dataset, Datasource, Report, Query, File, Job, Visual, Assistant, Library, Template). Payload is { to } (a principal id), required. Before moving, in-progress draft rows owned by {id} (Dataset/Report/Query with editingId set) are destroyed so half-edited drafts are not transferred. The caller must hold the `reassign` permission on the SOURCE principal (superuser, the user themself, or a team admin), else 401. Runs in a route transaction and returns 200 with the array of reassigned entities (HAL inf:reassign). Captured against a throwaway source user (casey.morgan) owning a single directory File, reassigned to admin.
Request body
{
"to": "admin"
}
Response · 200
[
{
"extension": "",
"tenant": "acme",
"id": "00000000-0000-4000-8000-000000000001",
"filename": "Casey Folder",
"embedded": false,
"contentType": "false",
"lobId": null,
"role": null,
"description": null,
"data": {},
"createdById": null,
"letterheadId": null,
"jobActionAttachmentId": null,
"jobActionId": null,
"oauthClientId": null,
"assistantId": null,
"directory": true,
"indexed": false,
"indexingStartedAt": null,
"indexingErroredAt": null,
"indexingErrorMessage": null,
"indexedAt": null,
"parentId": null,
"libraryId": null,
"remoteId": null,
"remoteUrl": null,
"size": null,
"modifiedAt": null,
"messageId": null,
"chatId": null,
"expiresAt": null,
"createdAt": "2026-01-15T16:20:00.000Z",
"updatedAt": "2026-01-15T16:20:00.000Z",
"ownerId": "admin",
"templateId": null,
"integrationId": null
}
]
Captured from the API examples test suite; ids and timestamps are normalized.