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-listGET /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:
| Field | Type | Description |
|---|---|---|
id | string | The Principal id (username for a User, Team uuid for a Team, * for Everyone) |
userId | string | null | The username when the row is a User, else null |
teamId | string | null | The Team uuid when the row is a Team, else null |
type | string | User, Team, or Everyone |
name | string | Display label, resolved as team.name || displayName || username |
username | string | null | Username (User rows only) |
displayName | string | null | Display name (User rows only) |
email | string | null | Email (User rows only) |
initials | string | Avatar initials, stamped by addAvatarData (User rows) |
avatarColor | object | Themed avatar disc colors (background, text), stamped by addAvatarData (User rows) |
colorIndex | number | Index into the avatar color palette (User rows) |
avatarUrl | string | Uploaded avatar URL, present only when the User has uploaded an avatar |
color | string | null | Team color (Team rows) |
icon | string | null | Team icon (Team rows) |
materialIcon | string | null | Team Material icon name (Team rows) |
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.
[
{
"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"
]
GET /api/principals/{id}
Resolve a Principal id to its underlying resource.
Authentication: Required (open within the tenant, no permission check)
Path Parameters:
| Parameter | Type | Description |
|---|---|---|
id | string | The 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.
""
The Team case behaves identically, redirecting to the Team resource instead:
""
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:
| Parameter | Type | Description |
|---|---|---|
id | string | The source Principal id whose owned content is moved |
Request Body:
| Field | Type | Required | Description |
|---|---|---|---|
to | string | Yes | The 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.
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.
{
"to": "admin"
}
[
{
"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
}
]