Tenant / Admin API Overview
Administrative endpoints for managing a Tenant's settings, license, mail
configuration, App resource catalog, and (for managers) the Tenant lifecycle
itself. All routes are prefixed with /api.
These endpoints split into two auth tiers:
- Per-Tenant settings (settings, mail, App settings/resources, license
read/test/sync) require Superuser on the caller's OWN Tenant
(
permission.tenant.superuser/permission.tenant.write). A small number of reads are public within the Tenant. - Tenant lifecycle (create, delete, search-cluster reassignment, App-database
reassignment, job-history purge, maintenance pauses, App origin moves) requires
the root manager superuser (the
adminuser on themanagerTenant). These operate across Tenants.
PUT /api/settings edits the caller's own Tenant, resolved from credentials.
PUT /api/tenants/{id} resolves an explicit Tenant by id and edits it. Because
the latter opens a route transaction that sets the DB role before the lookup
runs, the Tenant row-level security policy (id = CURRENT_USER) prevents a
manager from resolving a different Tenant through it. Cross-Tenant edits that
must bypass that trap use the dedicated transaction-free routes
(search-cluster, app-database).
GET /api/tenants/{id}
Read a single Tenant's non-sensitive fields.
Authentication: Any authenticated session (resolved through tenant.lookup
under the public scope)
Response:
The Tenant body includes its id, name, the decoded license claims, the
mail config, AI/usage knobs (aiEnabled, aiMonthlyLimit, defaultAiProfileId,
aiSettings), session/security policy, export settings, and a computed
permissions block for the caller.
For a caller who is not a superuser, the systemId and maxConcurrentJobs fields are
left out, and so are the license's customer, seat and capacity claims (systemId,
issuedTo, namedUsers, maxConcurrentJobs); the entitlement claims (modules,
features, and the rest) stay.
Every Tenant carries appSubdomains, whether its App origins mint on per-Tenant
subdomains (see POST /api/tenants/{id}/_remint-app-origins below). For the root
manager superuser, a Tenant other than manager also carries its maintenance
hold (null while it is open) and the inf:tenant-maintenance, inf:tenant-pause
and inf:tenant-unpause links. Nobody else sees the hold.
{
"_links": {
"self": {
"href": "https://informer.example.com/api/tenants/acme"
}
},
"activated": false,
"permissions": {
"edit": true,
"delete": true,
"write": true,
"superuser": true,
"import": true,
"manager": false
},
"aiMonthlyLimit": 0,
"id": "acme",
"name": "acme",
"systemId": "00000000-0000-4000-8000-000000000001",
"license": {
"exp": 1800376947.68,
"iat": 1779287915,
"byok": false,
"type": "enterprise",
"status": "full",
"modules": {
"apps": true,
"salesforce": true
},
"product": "i5",
"features": {
"token": true,
"visuals": true,
"database": true,
"discover": true,
"drilldown": true,
"templates": true,
"dashboards": true,
"datasource": true
},
"issuedTo": "Entrinsik (Informer Testing - Tenant)",
"systemId": "00000000-0000-4000-8000-000000000001",
"developer": false,
"enterprise": true,
"multiTenant": false,
"renewalDate": 1797784947.68,
"integrations": [],
"usageBilling": {
"agent_run": false,
"ai_tokens": false,
"app_compute": false,
"app_storage": false,
"job_compute": false,
"file_storage": false,
"es_index_count": false,
"workspace_storage": false
}
},
"maxConcurrentJobs": 5,
"maxJobRetries": 3,
"mail": {
"host": "smtp.example.com",
"pool": true,
"port": 587,
"secure": false,
"authType": "basic",
"requireTLS": true,
"maxMessages": 100,
"maxConnections": 5,
"alwaysUseDefault": false,
"defaultFromAddress": "noreply@example.com"
},
"helpUrl": "https://docs.entrinsik.com/",
"helpEmail": "",
"passwordStrength": "Weak",
"queryDatasetTTL": 60,
"jobHistorySize": 200,
"sanitizeDataHtml": true,
"jobsEnabled": true,
"secureDatasetFields": false,
"logLevelSettings": {},
"loginAttemptsAllowed": 5,
"lockoutTime": 60,
"passwordResetTime": 90,
"queryAuditEnabled": false,
"requestAuditEnabled": false,
"sessionExpireMinutes": 720,
"allowSessionBump": true,
"maxUploadFileSizeMB": 20,
"defaultLetterheadId": null,
"defaultTeamId": null,
"defaultDomainId": null,
"crossTenantAccess": true,
"oauthDynamicRegistration": false,
"allowQuerySharing": false,
"aiEnabled": false,
"defaultAiProfileId": "00000000-0000-4000-8000-000000000002",
"aiSettings": {},
"excelExportSettings": {
"locked": {},
"headers": {
"showFilters": true,
"showExportName": true,
"showDescription": true,
"showInputParams": true,
"showGeneratedDate": true,
"showOrganizationName": true
},
"styling": {},
"includeHeaders": false,
"organizationName": null
},
"restrictExportTypes": false,
"allowedExportTypes": [],
"scheduleInJobTimeZone": false,
"cloudEnabled": false,
"cloudUrl": null,
"cloudToken": null,
"jwtPublicKey": null,
"frameAncestors": null,
"searchClusterId": "00000000-0000-4000-8000-000000000003",
"appDatabaseId": null,
"allowTclCommands": false,
"createdAt": "2026-01-15T16:20:00.000Z",
"updatedAt": "2026-01-15T16:20:00.000Z"
}
GET /api/tenants
List Tenants as a HAL collection under _embedded["inf:tenant"].
Authentication: Required
Query Parameters:
| Parameter | Type | Description |
|---|---|---|
crossTenantAccessOnly | boolean | Filter to Tenants with the crossTenantAccess flag (only meaningful for a manager view) |
Response:
For a non-manager caller the where clause pins the result to the caller's own
Tenant, so the collection returns exactly one row regardless of the
crossTenantAccessOnly query param. Only a root manager superuser on the
manager Tenant sees the full cross-Tenant list. Rows have the shape of
GET /api/tenants/{id}, so rollout tooling can read each Tenant's appSubdomains
from the list.
{
"_links": {
"self": {
"href": "https://informer.example.com/api/tenants"
}
},
"start": 0,
"count": 1,
"total": 1,
"_embedded": {
"inf:tenant": [
{
"_links": {
"self": {
"href": "https://informer.example.com/api/tenants/acme"
}
},
"activated": false,
"permissions": {
"edit": true,
"delete": true,
"write": true,
"superuser": true,
"import": true,
"manager": false
},
"aiMonthlyLimit": 0,
"id": "acme",
"name": "acme",
"systemId": "00000000-0000-4000-8000-000000000001",
"license": {
"exp": 1800376947.68,
"iat": 1779287915,
"byok": false,
"type": "enterprise",
"status": "full",
"modules": {
"apps": true,
"salesforce": true
},
"product": "i5",
"features": {
"token": true,
"visuals": true,
"database": true,
"discover": true,
"drilldown": true,
"templates": true,
"dashboards": true,
"datasource": true
},
"issuedTo": "Entrinsik (Informer Testing - Tenant)",
"systemId": "00000000-0000-4000-8000-000000000001",
"developer": false,
"enterprise": true,
"multiTenant": false,
"renewalDate": 1797784947.68,
"integrations": [],
"usageBilling": {
"agent_run": false,
"ai_tokens": false,
"app_compute": false,
"app_storage": false,
"job_compute": false,
"file_storage": false,
"es_index_count": false,
"workspace_storage": false
}
},
"maxConcurrentJobs": 5,
"maxJobRetries": 3,
"mail": {
"host": "smtp.example.com",
"pool": true,
"port": 587,
"secure": false,
"authType": "basic",
"requireTLS": true,
"maxMessages": 100,
"maxConnections": 5,
"alwaysUseDefault": false,
"defaultFromAddress": "noreply@example.com"
},
"helpUrl": "https://docs.entrinsik.com/",
"helpEmail": "",
"passwordStrength": "Weak",
"queryDatasetTTL": 60,
"jobHistorySize": 200,
"sanitizeDataHtml": true,
"jobsEnabled": true,
"secureDatasetFields": false,
"logLevelSettings": {},
"loginAttemptsAllowed": 5,
"lockoutTime": 60,
"passwordResetTime": 90,
"queryAuditEnabled": false,
"requestAuditEnabled": false,
"sessionExpireMinutes": 720,
"allowSessionBump": true,
"maxUploadFileSizeMB": 20,
"defaultLetterheadId": null,
"defaultTeamId": null,
"defaultDomainId": null,
"crossTenantAccess": true,
"oauthDynamicRegistration": false,
"allowQuerySharing": false,
"aiEnabled": false,
"defaultAiProfileId": "00000000-0000-4000-8000-000000000002",
"aiSettings": {},
"excelExportSettings": {
"locked": {},
"headers": {
"showFilters": true,
"showExportName": true,
"showDescription": true,
"showInputParams": true,
"showGeneratedDate": true,
"showOrganizationName": true
},
"styling": {},
"includeHeaders": false,
"organizationName": null
},
"restrictExportTypes": false,
"allowedExportTypes": [],
"scheduleInJobTimeZone": false,
"cloudEnabled": false,
"cloudUrl": null,
"cloudToken": null,
"jwtPublicKey": null,
"frameAncestors": null,
"searchClusterId": "00000000-0000-4000-8000-000000000003",
"appDatabaseId": null,
"allowTclCommands": false,
"createdAt": "2026-01-15T16:20:00.000Z",
"updatedAt": "2026-01-15T16:20:00.000Z"
}
]
}
}
POST /api/tenants/_id
Suggest a unique Tenant id from a proposed name.
Authentication: Required
Request Body:
| Field | Type | Required | Description |
|---|---|---|---|
name | string | Yes | The proposed Tenant name to slugify |
The route slugifies the name and appends a numeric suffix until the id is unique across all Tenants. The response body is the suggested id string.
Example Request:
{ "name": "New Corporation" }
{
"name": "New Corporation"
}
"new-corporation"
GET /api/settings
Read the caller's own Tenant settings.
Authentication: Required (the route is open: true)
Response:
The broad settings object (letterhead, export options, cloud, AI/usage knobs,
security policy, and more). The route eager-loads defaultTeam, defaultDomain,
defaultLetterhead, and mailIntegration, and projects a derived byok boolean
from the signed license. As with GET /api/tenants/{id}, a
caller who is not a superuser gets the license without its customer, seat and
capacity claims, and without the systemId and maxConcurrentJobs fields.
The AI/usage-related fields are:
| Field | Type | Description |
|---|---|---|
aiEnabled | boolean | Master switch for AI features on the Tenant |
defaultAiProfileId | UUID | null | The usage profile applied to users with no team-assigned profile |
aiMonthlyLimit | number | Tenant-wide monthly credit ceiling |
aiSettings | object | Per-feature AI configuration/instructions |
{
"_links": {
"self": {
"href": "https://informer.example.com/api/settings"
}
},
"activated": false,
"permissions": {
"edit": true,
"delete": true,
"write": true,
"superuser": true,
"import": true,
"manager": false
},
"aiMonthlyLimit": 0,
"id": "acme",
"name": "acme",
"systemId": "00000000-0000-4000-8000-000000000001",
"license": {
"exp": 1800376947.68,
"iat": 1779287915,
"byok": false,
"type": "enterprise",
"status": "full",
"modules": {
"apps": true,
"salesforce": true
},
"product": "i5",
"features": {
"token": true,
"visuals": true,
"database": true,
"discover": true,
"drilldown": true,
"templates": true,
"dashboards": true,
"datasource": true
},
"issuedTo": "Entrinsik (Informer Testing - Tenant)",
"systemId": "00000000-0000-4000-8000-000000000001",
"developer": false,
"enterprise": true,
"multiTenant": false,
"renewalDate": 1797784947.68,
"integrations": [],
"usageBilling": {
"agent_run": false,
"ai_tokens": false,
"app_compute": false,
"app_storage": false,
"job_compute": false,
"file_storage": false,
"es_index_count": false,
"workspace_storage": false
}
},
"maxConcurrentJobs": 5,
"maxJobRetries": 3,
"mail": {
"host": "smtp.example.com",
"pool": true,
"port": 587,
"secure": false,
"authType": "basic",
"requireTLS": true,
"maxMessages": 100,
"maxConnections": 5,
"alwaysUseDefault": false,
"defaultFromAddress": "noreply@example.com"
},
"helpUrl": "https://docs.entrinsik.com/",
"helpEmail": "",
"passwordStrength": "Weak",
"queryDatasetTTL": 60,
"jobHistorySize": 200,
"sanitizeDataHtml": true,
"jobsEnabled": true,
"secureDatasetFields": false,
"logLevelSettings": {},
"loginAttemptsAllowed": 5,
"lockoutTime": 60,
"passwordResetTime": 90,
"queryAuditEnabled": false,
"requestAuditEnabled": false,
"sessionExpireMinutes": 720,
"allowSessionBump": true,
"maxUploadFileSizeMB": 20,
"defaultLetterheadId": null,
"defaultTeamId": null,
"defaultDomainId": null,
"crossTenantAccess": true,
"oauthDynamicRegistration": false,
"allowQuerySharing": false,
"aiEnabled": false,
"defaultAiProfileId": "00000000-0000-4000-8000-000000000002",
"aiSettings": {},
"excelExportSettings": {
"locked": {},
"headers": {
"showFilters": true,
"showExportName": true,
"showDescription": true,
"showInputParams": true,
"showGeneratedDate": true,
"showOrganizationName": true
},
"styling": {},
"includeHeaders": false,
"organizationName": null
},
"restrictExportTypes": false,
"allowedExportTypes": [],
"scheduleInJobTimeZone": false,
"cloudEnabled": false,
"cloudUrl": null,
"cloudToken": null,
"jwtPublicKey": null,
"frameAncestors": null,
"searchClusterId": "00000000-0000-4000-8000-000000000003",
"appDatabaseId": null,
"allowTclCommands": false,
"createdAt": "2026-01-15T16:20:00.000Z",
"updatedAt": "2026-01-15T16:20:00.000Z",
"defaultTeam": null,
"defaultDomain": null,
"defaultLetterhead": null,
"mailIntegration": null,
"byok": false
}
PUT /api/settings
Update the caller's own Tenant settings.
Authentication: Superuser for any field; billing managers (billing.manage)
may set only aiEnabled, aiSettings, aiMonthlyLimit, and defaultAiProfileId
Request Body:
The payload is stripUnknown, so only whitelisted fields persist. Notably
searchClusterId is not one of them: use PUT /api/tenants/{id}/search-cluster
to reassign a search cluster.
The AI/usage fields a billing manager may set:
| Field | Type | Description |
|---|---|---|
aiEnabled | boolean | Enable/disable AI for the Tenant |
defaultAiProfileId | UUID | null | Set or clear the Tenant default usage profile |
aiMonthlyLimit | number (≥ 0) | Tenant-wide monthly credit ceiling |
aiSettings | object | Per-feature AI configuration |
Other notable Superuser-only fields (not writable by a billing manager):
| Field | Type | Description |
|---|---|---|
maxJobRetries | integer (≥ 0) | Retries allowed for a one-time Job that fails before it is retired without having run (default 3). 0 means run once and never retry. A Job's attempts counter includes the first run, so the budget is one attempt plus this many. |
oauthDynamicRegistration | boolean | Allows OAuth clients (MCP clients in particular) to self-register via POST /api/oauth/register. While off, that route answers 404 and registration_endpoint is omitted from the discovery metadata. Surfaced in the UI under Administration > Settings > Security. See MCP Clients. |
Example Request:
{ "sessionExpireMinutes": 120, "allowSessionBump": true }
Response:
Responds 200 with the updated Tenant.
{
"sessionExpireMinutes": 120,
"allowSessionBump": true
}
{
"_links": {
"self": {
"href": "https://informer.example.com/api/settings"
}
},
"activated": false,
"permissions": {
"edit": true,
"delete": true,
"write": true,
"superuser": true,
"import": true,
"manager": false
},
"aiMonthlyLimit": 0,
"id": "acme",
"name": "acme",
"systemId": "00000000-0000-4000-8000-000000000001",
"license": {
"byok": false,
"modules": {
"salesforce": true,
"apps": true
},
"features": {
"token": true,
"discover": true,
"dashboards": true,
"visuals": true,
"drilldown": true,
"templates": true,
"datasource": true,
"database": true
},
"developer": false,
"enterprise": true,
"multiTenant": false,
"integrations": [],
"usageBilling": {
"agent_run": false,
"ai_tokens": false,
"app_compute": false,
"job_compute": false,
"app_storage": false,
"es_index_count": false,
"file_storage": false,
"workspace_storage": false
},
"exp": 1800376947.68,
"renewalDate": 1797784947.68,
"systemId": "00000000-0000-4000-8000-000000000001",
"status": "full",
"type": "enterprise",
"product": "i5",
"issuedTo": "Entrinsik (Informer Testing - Tenant)",
"iat": 1779287915
},
"maxConcurrentJobs": 5,
"maxJobRetries": 3,
"mail": {
"host": "smtp.example.com",
"pool": true,
"port": 587,
"secure": false,
"authType": "basic",
"requireTLS": true,
"maxMessages": 100,
"maxConnections": 5,
"alwaysUseDefault": false,
"defaultFromAddress": "noreply@example.com"
},
"helpUrl": "https://docs.entrinsik.com/",
"helpEmail": "",
"passwordStrength": "Weak",
"queryDatasetTTL": 60,
"jobHistorySize": 200,
"sanitizeDataHtml": true,
"jobsEnabled": true,
"secureDatasetFields": false,
"logLevelSettings": {},
"loginAttemptsAllowed": 5,
"lockoutTime": 60,
"passwordResetTime": 90,
"queryAuditEnabled": false,
"requestAuditEnabled": false,
"sessionExpireMinutes": 120,
"allowSessionBump": true,
"maxUploadFileSizeMB": 20,
"defaultLetterheadId": null,
"defaultTeamId": null,
"defaultDomainId": null,
"crossTenantAccess": true,
"oauthDynamicRegistration": false,
"allowQuerySharing": false,
"aiEnabled": false,
"defaultAiProfileId": "00000000-0000-4000-8000-000000000002",
"aiSettings": {},
"excelExportSettings": {
"locked": {},
"headers": {
"showFilters": true,
"showExportName": true,
"showDescription": true,
"showInputParams": true,
"showGeneratedDate": true,
"showOrganizationName": true
},
"styling": {},
"includeHeaders": false,
"organizationName": null
},
"restrictExportTypes": false,
"allowedExportTypes": [],
"scheduleInJobTimeZone": false,
"cloudEnabled": false,
"cloudUrl": null,
"cloudToken": null,
"jwtPublicKey": null,
"frameAncestors": null,
"searchClusterId": "00000000-0000-4000-8000-000000000003",
"appDatabaseId": null,
"allowTclCommands": false,
"createdAt": "2026-01-15T16:20:00.000Z",
"updatedAt": "2026-01-15T16:20:00.000Z",
"mailIntegration": null
}
GET /api/settings/default-domain
Read just the Tenant's default domain id.
Authentication: Required (the route is open: true, tenant auth strategy)
Response:
Returns the defaultDomainId as { id } (which may be null).
{
"_links": {
"self": {
"href": "https://informer.example.com/api/settings/default-domain"
}
},
"id": null
}
GET /api/settings/mail
Read the Tenant's SMTP configuration (the mail JSON column).
Authentication: Required
Mail settings live at /api/settings/mail, not /api/mail-settings.
Response:
{
"_links": {
"self": {
"href": "https://informer.example.com/api/settings/mail"
}
},
"host": "smtp.example.com",
"pool": true,
"port": 587,
"secure": false,
"authType": "basic",
"requireTLS": true,
"maxMessages": 100,
"maxConnections": 5,
"alwaysUseDefault": false,
"defaultFromAddress": "noreply@example.com"
}
PUT /api/settings/mail
Update the Tenant's SMTP configuration.
Authentication: permission.tenant.superuser
Request Body:
The payload is stripUnknown and applies Joi defaults. authType must be one of
the registered mail-auth driver ids.
| Field | Type | Description |
|---|---|---|
authType | string | One of basic, oauth, gmail, microsoft-mail, none (default none). basic is SMTP username/password auth |
host | string | SMTP host (default localhost) |
port | integer | SMTP port (default 587) |
secure | boolean | Use TLS on connect (default false) |
ignoreTLS | boolean | Ignore TLS (default false) |
requireTLS | boolean | Require TLS (default false) |
pool | boolean | Use connection pooling (default true) |
maxConnections | integer | Max connections (default 5, min 1) |
maxMessages | integer | Max messages per connection (default 100, min 1) |
rateLimit | integer | Messages per rateDelta (optional) |
rateDelta | integer | Rate-limit window in ms (default 1000, min 1) |
defaultFromAddress | string | Default from address |
alwaysUseDefault | boolean | Always use the default from address (default false) |
auth | object | Driver-specific authentication credentials |
Example Request:
{
"authType": "basic",
"host": "smtp.example.com",
"port": 587,
"secure": false,
"requireTLS": true,
"defaultFromAddress": "noreply@example.com"
}
Response:
Responds 200 with the saved mail object.
{
"authType": "basic",
"host": "smtp.example.com",
"port": 587,
"secure": false,
"requireTLS": true,
"defaultFromAddress": "noreply@example.com"
}
{
"_links": {
"self": {
"href": "https://informer.example.com/api/settings/mail"
}
},
"authType": "basic",
"host": "smtp.example.com",
"port": 587,
"secure": false,
"requireTLS": true,
"defaultFromAddress": "noreply@example.com",
"pool": true,
"ignoreTLS": false,
"alwaysUseDefault": false,
"maxConnections": 5,
"maxMessages": 100,
"rateDelta": 1000
}
DELETE /api/settings/mail
Clear the Tenant's mail configuration.
Authentication: permission.tenant.superuser
Response:
Resets mail to {} and returns the cleared object. Like all Informer DELETE
routes it answers 200 (never 204), and it swallows errors (always 200).
{
"_links": {
"self": {
"href": "https://informer.example.com/api/settings/mail"
}
}
}
GET /api/settings/apps
Read the Tenant's App CDN/CSP configuration.
Authentication: Required
Response:
The App settings (cdns, csp, customOrigins) merged with defaults, plus a
_defaults block the UI uses for its reset affordance. New Tenants default to
the tightest CSP mode (approved-only).
{
"_links": {
"self": {
"href": "https://informer.example.com/api/settings/apps"
}
},
"cdns": {
"d3": {
"enabled": true
},
"cdnjs": {
"enabled": true
},
"unpkg": {
"enabled": false
},
"google": {
"enabled": false
},
"plotly": {
"enabled": true
},
"jsdelivr": {
"enabled": true
}
},
"csp": {
"mode": "approved-only",
"requireSri": false
},
"customOrigins": [],
"_defaults": {
"cdns": {
"jsdelivr": {
"enabled": true
},
"cdnjs": {
"enabled": true
},
"unpkg": {
"enabled": false
},
"google": {
"enabled": false
},
"d3": {
"enabled": true
},
"plotly": {
"enabled": true
}
},
"csp": {
"mode": "approved-only",
"requireSri": false
},
"customOrigins": []
}
}
PUT /api/settings/apps
Update the Tenant's App settings.
Authentication: permission.tenant.superuser
Request Body:
cdns and csp are deep-merged (partial update). customOrigins is
replaced wholesale.
Each customOrigins entry's origin must be an https URL with no wildcard
host, and is stored as its bare origin (path, query, and credentials are
stripped). Invalid entries return 400; at most 50 are accepted.
Example Request:
{ "csp": { "mode": "any-enabled-cdn" } }
Response:
Responds 200 with the merged settings (no _defaults block).
{
"csp": {
"mode": "any-enabled-cdn"
}
}
{
"_links": {
"self": {
"href": "https://informer.example.com/api/settings/apps"
}
},
"cdns": {
"d3": {
"enabled": true
},
"cdnjs": {
"enabled": true
},
"unpkg": {
"enabled": false
},
"google": {
"enabled": false
},
"plotly": {
"enabled": true
},
"jsdelivr": {
"enabled": true
}
},
"csp": {
"mode": "any-enabled-cdn",
"requireSri": false
},
"customOrigins": []
}
GET /api/settings/apps/resources
List the curated CDN library catalog for Apps.
Authentication: Required
Query Parameters:
| Parameter | Type | Description |
|---|---|---|
enabled | boolean | Filter by enabled state |
featured | boolean | Filter by featured flag |
category | string | Filter by category (e.g. visualization, ui, data, utility, maps) |
Response:
A HAL collection under _embedded["inf:app-resource"], sorted featured-first,
then by name, then version. Each row carries its resources (script/style/asset
URLs with SRI hashes) and a default set of common libraries ships pre-seeded
(Chart.js, D3.js, Highcharts, and others).
{
"_links": {
"self": {
"href": "https://informer.example.com/api/settings/apps/resources"
},
"inf:app-resource": {
"href": "https://informer.example.com/api/settings/apps/resources/"
},
"inf:refresh-sri": {
"href": "https://informer.example.com/api/settings/apps/resources//_refresh-sri"
}
},
"_embedded": {
"inf:app-resource": [
{
"_links": {
"self": {
"href": "https://informer.example.com/api/settings/apps/resources/00000000-0000-4000-8000-000000000001"
},
"inf:refresh-sri": {
"href": "https://informer.example.com/api/settings/apps/resources/00000000-0000-4000-8000-000000000001/_refresh-sri"
}
},
"id": "00000000-0000-4000-8000-000000000001",
"name": "AG Grid Community",
"version": "34.3.1",
"slug": "ag-grid-community",
"description": "The Best JavaScript Data Table for Building Enterprise Applications",
"category": "data",
"aiHint": "Use for large datasets with sorting, filtering, grouping, aggregation, and virtual scrolling. Best enterprise-grade data grid for performance and features.",
"resources": {
"assets": [],
"styles": [
{
"url": "https://cdn.jsdelivr.net/npm/ag-grid-community@34.3.1/styles/ag-grid.min.css",
"integrity": "sha384-13U4UwTfomIc71W7P5GUq+AJx9emviNMsM0bNEFXvta0eHOzShlVom4hueraQHct"
},
{
"url": "https://cdn.jsdelivr.net/npm/ag-grid-community@34.3.1/styles/ag-theme-quartz.min.css",
"integrity": "sha384-IvPmV7O3nZI9BmOHg6IO6h52homvd9dy6+olMn01hI29QRK+LZonwq3+LMToIyMg"
}
],
"scripts": [
{
"url": "https://cdn.jsdelivr.net/npm/ag-grid-community@34.3.1/dist/ag-grid-community.min.js",
"module": false,
"integrity": "sha384-1m5FM32cqWvkVPC2kdiP5dLsQUO3OCeEAGgZ4RqzCWlsWcCVIM9HXAjD1A99GPs3"
}
]
},
"licenseKey": null,
"enabled": true,
"featured": true,
"capturedAt": "2026-01-15T16:20:00.000Z",
"capturedBy": "admin",
"createdAt": "2026-01-15T16:20:00.000Z",
"updatedAt": "2026-01-15T16:20:00.000Z",
"tenant": "acme"
},
{
"_links": {
"self": {
"href": "https://informer.example.com/api/settings/apps/resources/00000000-0000-4000-8000-000000000002"
},
"inf:refresh-sri": {
"href": "https://informer.example.com/api/settings/apps/resources/00000000-0000-4000-8000-000000000002/_refresh-sri"
}
},
"id": "00000000-0000-4000-8000-000000000002",
"name": "Animate.css",
"version": "4.1.1",
"slug": "animate-css",
"description": "A cross-browser library of CSS animations",
"category": "ui",
"aiHint": "Use for ready-to-use CSS animations. Add class \"animate__animated\" plus animation class like \"animate__bounce\", \"animate__fadeIn\", \"animate__slideInUp\", etc.",
"resources": {
"assets": [],
"styles": [
{
"url": "https://cdnjs.cloudflare.com/ajax/libs/animate.css/4.1.1/animate.min.css",
"integrity": "sha384-Gu3KVV2H9d+yA4QDpVB7VcOyhJlAVrcXd0thEjr4KznfaFPLe0xQJyonVxONa4ZC"
}
],
"scripts": []
},
"licenseKey": null,
"enabled": true,
"featured": true,
"capturedAt": "2026-01-15T16:20:00.000Z",
"capturedBy": "admin",
"createdAt": "2026-01-15T16:20:00.000Z",
"updatedAt": "2026-01-15T16:20:00.000Z",
"tenant": "acme"
},
{
"_links": {
"self": {
"href": "https://informer.example.com/api/settings/apps/resources/00000000-0000-4000-8000-000000000003"
},
"inf:refresh-sri": {
"href": "https://informer.example.com/api/settings/apps/resources/00000000-0000-4000-8000-000000000003/_refresh-sri"
}
},
"id": "00000000-0000-4000-8000-000000000003",
"name": "AOS",
"version": "2.3.4",
"slug": "aos",
"description": "Animate On Scroll library for scroll-triggered animations",
"category": "ui",
"aiHint": "Use for scroll-triggered animations. Add data-aos=\"fade-up\" (or other animation) to elements. Initialize with AOS.init(). Options: duration, easing, delay, offset.",
"resources": {
"assets": [],
"styles": [
{
"url": "https://cdnjs.cloudflare.com/ajax/libs/aos/2.3.4/aos.css",
"integrity": "sha384-/rJKQnzOkEo+daG0jMjU1IwwY9unxt1NBw3Ef2fmOJ3PW/TfAg2KXVoWwMZQZtw9"
}
],
"scripts": [
{
"url": "https://cdnjs.cloudflare.com/ajax/libs/aos/2.3.4/aos.js",
"module": false,
"integrity": "sha384-n1AULnKdMJlK1oQCLNDL9qZsDgXtH6jRYFCpBtWFc+a9Yve0KSoMn575rk755NJZ"
}
]
},
"licenseKey": null,
"enabled": true,
"featured": true,
"capturedAt": "2026-01-15T16:20:00.000Z",
"capturedBy": "admin",
"createdAt": "2026-01-15T16:20:00.000Z",
"updatedAt": "2026-01-15T16:20:00.000Z",
"tenant": "acme"
},
"… 26 more items (29 total) — omitted from docs"
]
}
}
POST /api/settings/apps/resources
Add an App resource to the catalog.
Authentication: permission.tenant.superuser
Request Body:
| Field | Type | Required | Description |
|---|---|---|---|
name | string | Yes | Display name |
version | string | Yes | Library version |
slug | string | No | URL-safe id; auto-derived from name when omitted |
description | string | No | Description |
category | string | No | Catalog category |
aiHint | string | No | Guidance the AI uses when choosing/using the library |
resources | object | No | { scripts, styles, assets } URL lists |
licenseKey | string | No | License key for commercial libraries |
enabled | boolean | No | Whether the resource is selectable |
featured | boolean | No | Whether the resource is featured |
The catalog enforces a unique (slug, version) per Tenant. Each resources
entry must pin a version in its URL and carry exactly one of integrity (an
SRI hash) or skipIntegrity: true. The route enforces SRI so a CDN swap cannot
silently inject new bytes. When resources are supplied, capturedBy is set to
the caller and capturedAt is stamped.
Example Request:
{
"name": "Acme Charts",
"version": "2.1.0",
"category": "visualization",
"aiHint": "Example custom charting library, registered in the App resource catalog so Apps can load it.",
"resources": {
"scripts": [{
"url": "https://cdn.example.com/acme-charts@2.1.0/dist/acme-charts.min.js",
"integrity": "sha384-exampleSRIhashReplaceWithTheRealHashFromYourCDNorBuildTool"
}],
"styles": [],
"assets": []
}
}
Response:
Responds 200 (not 201) with the created resource.
{
"name": "Acme Charts",
"version": "2.1.0",
"category": "visualization",
"aiHint": "Example custom charting library, registered in the App resource catalog so Apps can load it.",
"resources": {
"scripts": [
{
"url": "https://cdn.example.com/acme-charts@2.1.0/dist/acme-charts.min.js",
"integrity": "sha384-exampleSRIhashReplaceWithTheRealHashFromYourCDNorBuildTool"
}
],
"styles": [],
"assets": []
}
}
{
"_links": {
"self": {
"href": "https://informer.example.com/api/settings/apps/resources"
},
"inf:app-resources": {
"href": "https://informer.example.com/api/settings/apps/resources"
}
},
"id": "00000000-0000-4000-8000-000000000001",
"tenant": "acme",
"name": "Acme Charts",
"version": "2.1.0",
"slug": "acme-charts",
"description": null,
"category": "visualization",
"aiHint": "Example custom charting library, registered in the App resource catalog so Apps can load it.",
"resources": {
"assets": [],
"styles": [],
"scripts": [
{
"url": "https://cdn.example.com/acme-charts@2.1.0/dist/acme-charts.min.js",
"module": false,
"integrity": "sha384-exampleSRIhashReplaceWithTheRealHashFromYourCDNorBuildTool"
}
]
},
"licenseKey": null,
"enabled": true,
"featured": false,
"capturedAt": "2026-01-15T16:20:00.000Z",
"capturedBy": "admin",
"updatedAt": "2026-01-15T16:20:00.000Z",
"createdAt": "2026-01-15T16:20:00.000Z"
}
GET /api/settings/apps/resources/{id}
Read a single App resource.
Authentication: Required
Pre-blocks: appResource.lookup (404 if missing)
Response:
The catalog row with its resource URLs and SRI hashes.
{
"_links": {
"self": {
"href": "https://informer.example.com/api/settings/apps/resources/00000000-0000-4000-8000-000000000001"
},
"inf:app-resources": {
"href": "https://informer.example.com/api/settings/apps/resources/resources"
},
"inf:refresh-sri": {
"href": "https://informer.example.com/api/settings/apps/resources/00000000-0000-4000-8000-000000000001/_refresh-sri"
}
},
"id": "00000000-0000-4000-8000-000000000001",
"name": "Chart.js",
"version": "4.4.1",
"slug": "chartjs",
"description": "Simple yet flexible JavaScript charting library.",
"category": "visualization",
"aiHint": "Use for bar, line, and pie charts in App widgets.",
"resources": {
"assets": [],
"styles": [],
"scripts": [
{
"url": "https://cdn.jsdelivr.net/npm/chart.js@4.4.1/dist/chart.umd.min.js"
}
]
},
"licenseKey": null,
"enabled": true,
"featured": true,
"capturedAt": "2026-01-15T16:20:00.000Z",
"capturedBy": null,
"createdAt": "2026-01-15T16:20:00.000Z",
"updatedAt": "2026-01-15T16:20:00.000Z",
"tenant": "acme"
}
PUT /api/settings/apps/resources/{id}
Update an App resource.
Authentication: permission.tenant.superuser
Request Body:
Any subset of the resource fields (see POST). Omitting resources preserves
the stored lists, so a partial update such as { "name": "..." } does not
blank the catalog entry's scripts/styles/assets. When resources change,
capturedAt/capturedBy are re-stamped.
Example Request:
{ "description": "Simple yet flexible JavaScript charting (updated).", "featured": false }
Response:
Responds 200 with the updated resource.
{
"description": "Simple yet flexible JavaScript charting (updated).",
"featured": false
}
{
"_links": {
"self": {
"href": "https://informer.example.com/api/settings/apps/resources/00000000-0000-4000-8000-000000000001"
},
"inf:app-resources": {
"href": "https://informer.example.com/api/settings/apps/resources/resources"
}
},
"id": "00000000-0000-4000-8000-000000000001",
"name": "Chart.js",
"version": "4.4.1",
"slug": "chartjs",
"description": "Simple yet flexible JavaScript charting (updated).",
"category": "visualization",
"aiHint": "Use for bar, line, and pie charts in App widgets.",
"resources": {
"assets": [],
"styles": [],
"scripts": [
{
"url": "https://cdn.jsdelivr.net/npm/chart.js@4.4.1/dist/chart.umd.min.js"
}
]
},
"licenseKey": null,
"enabled": true,
"featured": false,
"capturedAt": "2026-01-15T16:20:00.000Z",
"capturedBy": null,
"createdAt": "2026-01-15T16:20:00.000Z",
"updatedAt": "2026-01-15T16:20:00.000Z",
"tenant": "acme"
}
DELETE /api/settings/apps/resources/{id}
Delete an App resource.
Authentication: permission.tenant.superuser
Response:
Removes the catalog row and responds 200 with { success: true } (not 204).
{
"_links": {
"self": {
"href": "https://informer.example.com/api/settings/apps/resources/00000000-0000-4000-8000-000000000001"
}
},
"success": true
}
GET /api/license
Read the current license.
Authentication: Required
Response:
For a superuser, returns the locally-decoded license claims plus a licenseManager
connectivity block. When the License Manager is unreachable (offline installs) the
handler swallows the error and falls back to just the decoded license, still 200.
Every other caller gets the decoded claims without the customer, seat and capacity
ones (systemId, issuedTo, namedUsers, maxConcurrentJobs) and without the
licenseManager block: the License Manager is not contacted for them.
{
"_links": {
"self": {
"href": "https://informer.example.com/api/license"
}
},
"byok": false,
"modules": {
"salesforce": true,
"apps": true
},
"features": {
"token": true,
"discover": true,
"dashboards": true,
"visuals": true,
"drilldown": true,
"templates": true,
"datasource": true,
"database": true
},
"developer": false,
"enterprise": true,
"multiTenant": false,
"integrations": [],
"usageBilling": {
"agent_run": false,
"ai_tokens": false,
"app_compute": false,
"job_compute": false,
"app_storage": false,
"es_index_count": false,
"file_storage": false,
"workspace_storage": false
},
"exp": 1800376947.68,
"renewalDate": 1797784947.68,
"systemId": "00000000-0000-4000-8000-000000000001",
"status": "full",
"type": "enterprise",
"product": "i5",
"issuedTo": "Entrinsik (Informer Testing - Tenant)",
"iat": 1779287915,
"activated": false
}
POST /api/license/_read
Decode a license token without installing it.
Authentication: permission.tenant.superuser
Request Body:
| Field | Type | Required | Description |
|---|---|---|---|
token | string | One of token / upload | The license JWT to verify and decode |
upload | string | One of token / upload | An uploaded license-file id |
Verifies the JWT against Entrinsik's public key and returns the decoded claims so
the admin UI can preview a token before applying it. Answers 400 when the token
is invalid.
Example Request:
{ "token": "eyJhbGciOiJIUzI1NiJ9.example-token.signature" }
{
"token": "eyJhbGciOiJIUzI1NiJ9.example-token-1.signature"
}
{
"_links": {
"self": {
"href": "https://informer.example.com/api/license/_read"
}
},
"byok": false,
"modules": {
"salesforce": true,
"apps": true
},
"features": {
"token": true,
"discover": true,
"dashboards": true,
"visuals": true,
"drilldown": true,
"templates": true,
"datasource": true,
"database": true
},
"developer": false,
"enterprise": true,
"multiTenant": false,
"integrations": [],
"usageBilling": {
"agent_run": false,
"ai_tokens": false,
"app_compute": false,
"job_compute": false,
"app_storage": false,
"es_index_count": false,
"file_storage": false,
"workspace_storage": false
},
"exp": 1800376947.68,
"renewalDate": 1797784947.68,
"systemId": "00000000-0000-4000-8000-000000000001",
"status": "full",
"type": "enterprise",
"product": "i5",
"issuedTo": "Entrinsik (Informer Testing - Tenant)",
"iat": 1779287915
}
POST /api/license/_test
Validate a license token (or uploaded file).
Authentication: permission.tenant.superuser
Request Body:
| Field | Type | Required | Description |
|---|---|---|---|
token | string | One of token / upload | The license token to validate |
upload | string | One of token / upload | An uploaded license-file id |
Validates without throwing the 423 a normal license check would. Used by the
install UI to validate before committing.
Response:
Responds 200 with an empty body when the token is valid; 422 when it is not.
{
"token": "eyJhbGciOiJIUzI1NiJ9.example-token-1.signature"
}
POST /api/license/_sync
Sync the Tenant's license with the License Manager.
Authentication: permission.tenant.superuser
Request Body:
| Field | Type | Required | Description |
|---|---|---|---|
activate | boolean | No | Default true: when the License Manager reports no activation for this install, the stored activation token is cleared. An activation token the License Manager hands back is adopted either way |
passive | boolean | No | Default false. Forwarded to the License Manager as a read-only sync that never mints an activation; read-mostly callers send { "activate": false, "passive": true } |
Round-trips to the License Manager to refresh the Tenant's license and AI entitlement cache. The License Manager's copy of the license is adopted only when it verifies strictly (expiry included) and differs from the token the Tenant already holds; an expired or unverifiable copy is refused and the stored license is kept.
Response:
Responds 200 with { updated, confirmed, reason? }:
updatedistrueonly when a different license was adopted, so callers can skip repaint work on a no-op sync.confirmedistruewhen the License Manager vouched for the license this install runs: it adopted a copy, or handed back the token already stored.reasonis present whenconfirmedisfalse:unusablewhen the License Manager holds a copy that was refused, orofflinewhen the install is configured offline and the call short-circuited with a503the handler swallows. In both cases nothing changes.
{}
{
"_links": {
"self": {
"href": "https://informer.example.com/api/license/_sync"
}
},
"updated": false,
"confirmed": false,
"reason": "offline"
}
GET /api/password-settings
Read the Tenant's configured minimum password strength.
Authentication: None (the route is auth: false)
Response:
Returns { passwordStrength }. This is a read of the configured policy, not a
password scorer.
{
"_links": {
"self": {
"href": "https://informer.example.com/api/password-settings"
}
},
"passwordStrength": "Weak"
}
POST /api/tenants
Create a Tenant.
Authentication: permission.tenants.createStrict (Superuser plus a license
with the multiTenant entitlement; the strict variant is deliberate, since the
non-strict form short-circuits for Superusers and would leave the license's
multiTenant flag unenforced)
Pre-blocks: permission.tenants.createStrict,
tenant.verifyLicense(payload.licenseToken) (400 on a bad token), existence
checks for copyFromId / bundleId (400), tenant.provision,
auto-activation
Request Body:
| Field | Type | Required | Description |
|---|---|---|---|
name | string | Yes | Tenant display name |
licenseToken | string | Yes | License token for the new Tenant |
id | string | No | Tenant id; slugified from name when omitted |
adminPassword | string | No | Initial admin password (default 123) |
adminEmail | string | No | Initial admin email |
progress | string | No | Progress-tracking id |
copyFromId | string | No | Copy content from an existing Tenant |
bundleId | string | No | Deploy from a bundle template |
jwtPublicKey | string | No | JWT public key for authentication |
activate | boolean | No | Auto-activate the license (root manager superuser only; default false) |
searchClusterId | string | No | Elasticsearch cluster id |
appDatabaseId | string (UUID) | null | No | Manager-curated App Database profile that hosts the Tenant's App workspace data. null (default) uses the deployment-level default; change it later via PUT /api/tenants/{id}/app-database |
tenant.provision creates the Tenant, an admin user, and the seed copy.
activate: true auto-activates only for a root manager superuser.
Example Request:
{
"id": "docs-newcorp",
"name": "New Corporation",
"licenseToken": "license-token-xyz"
}
Response:
Responds 201 Created with the new Tenant and a Location header.
{
"id": "docs-newcorp",
"name": "New Corporation",
"licenseToken": "eyJhbGciOiJIUzI1NiJ9.example-token-1.signature"
}
{
"_links": {
"self": {
"href": "https://informer.example.com/api/tenants/docs-newcorp"
}
},
"activated": false,
"permissions": {
"edit": true,
"delete": true,
"write": true,
"superuser": true,
"import": true,
"manager": true
},
"aiMonthlyLimit": 0,
"maxConcurrentJobs": 5,
"maxJobRetries": 3,
"mail": {},
"helpUrl": "https://docs.entrinsik.com/",
"helpEmail": "",
"passwordStrength": "Weak",
"queryDatasetTTL": 60,
"jobHistorySize": 200,
"sanitizeDataHtml": true,
"jobsEnabled": true,
"secureDatasetFields": true,
"logLevelSettings": {},
"queryAuditEnabled": true,
"crossTenantAccess": true,
"oauthDynamicRegistration": false,
"aiEnabled": false,
"aiSettings": {},
"excelExportSettings": {
"locked": {},
"headers": {
"showFilters": true,
"showExportName": true,
"showDescription": true,
"showInputParams": true,
"showGeneratedDate": true,
"showOrganizationName": true
},
"styling": {},
"includeHeaders": false,
"organizationName": null
},
"restrictExportTypes": false,
"allowedExportTypes": [],
"scheduleInJobTimeZone": false,
"cloudEnabled": false,
"allowTclCommands": false,
"id": "docs-newcorp",
"name": "New Corporation",
"searchClusterId": "00000000-0000-4000-8000-000000000001",
"appDatabaseId": null,
"systemId": "00000000-0000-4000-8000-000000000002",
"jwtPublicKey": null,
"updatedAt": "2026-01-15T16:20:00.000Z",
"createdAt": "2026-01-15T16:20:00.000Z",
"license": {
"exp": 1800376947.68,
"iat": 1779287915,
"byok": false,
"type": "enterprise",
"status": "full",
"modules": {
"apps": true,
"salesforce": true
},
"product": "i5",
"features": {
"token": true,
"visuals": true,
"database": true,
"discover": true,
"drilldown": true,
"templates": true,
"dashboards": true,
"datasource": true
},
"issuedTo": "Entrinsik (Informer Testing - Tenant)",
"systemId": "00000000-0000-4000-8000-000000000002",
"developer": false,
"enterprise": true,
"multiTenant": false,
"renewalDate": 1797784947.68,
"integrations": [],
"usageBilling": {
"agent_run": false,
"ai_tokens": false,
"app_compute": false,
"app_storage": false,
"job_compute": false,
"file_storage": false,
"es_index_count": false,
"workspace_storage": false
}
},
"defaultTeamId": null,
"defaultDomainId": null,
"loginAttemptsAllowed": 5,
"lockoutTime": 60,
"passwordResetTime": 90,
"requestAuditEnabled": null,
"sessionExpireMinutes": 720,
"allowSessionBump": true,
"maxUploadFileSizeMB": 20,
"defaultLetterheadId": null,
"allowQuerySharing": false,
"cloudUrl": null,
"cloudToken": null,
"aiAllUserAccess": false,
"frameAncestors": null,
"defaultAiProfileId": null
}
PUT /api/tenants/{id}
Update a Tenant resolved by id.
Authentication: Superuser
Pre-blocks: tenant.lookup(params.id), permission.tenant.write(pre.tenant)
Request Body:
Tenant configuration fields (the writable subset). Changing appSubdomains
answers 403, because only POST /api/tenants/{id}/_remint-app-origins sets it; a
body that repeats the current value saves, so a form can send back what it read.
A maintenance field is dropped: only the pause routes write it.
Example Request:
{ "name": "Acme Analytics" }
Response:
Responds 200 with the updated Tenant. The example below is captured as a Tenant
editing itself.
{
"name": "Acme Analytics"
}
{
"_links": {
"self": {
"href": "https://informer.example.com/api/tenants/acme"
}
},
"activated": false,
"permissions": {
"edit": true,
"delete": true,
"write": true,
"superuser": true,
"import": true,
"manager": false
},
"aiMonthlyLimit": 0,
"id": "acme",
"name": "Acme Analytics",
"systemId": "00000000-0000-4000-8000-000000000001",
"license": {
"byok": false,
"modules": {
"salesforce": true,
"apps": true
},
"features": {
"token": true,
"discover": true,
"dashboards": true,
"visuals": true,
"drilldown": true,
"templates": true,
"datasource": true,
"database": true
},
"developer": false,
"enterprise": true,
"multiTenant": false,
"integrations": [],
"usageBilling": {
"agent_run": false,
"ai_tokens": false,
"app_compute": false,
"job_compute": false,
"app_storage": false,
"es_index_count": false,
"file_storage": false,
"workspace_storage": false
},
"exp": 1800376947.68,
"renewalDate": 1797784947.68,
"systemId": "00000000-0000-4000-8000-000000000001",
"status": "full",
"type": "enterprise",
"product": "i5",
"issuedTo": "Entrinsik (Informer Testing - Tenant)",
"iat": 1779287915
},
"maxConcurrentJobs": 5,
"maxJobRetries": 3,
"mail": {},
"helpUrl": "https://docs.entrinsik.com/",
"helpEmail": "",
"passwordStrength": "Weak",
"queryDatasetTTL": 60,
"jobHistorySize": 200,
"sanitizeDataHtml": true,
"jobsEnabled": true,
"secureDatasetFields": false,
"logLevelSettings": {},
"loginAttemptsAllowed": 5,
"lockoutTime": 60,
"passwordResetTime": 90,
"queryAuditEnabled": false,
"requestAuditEnabled": false,
"sessionExpireMinutes": 120,
"allowSessionBump": true,
"maxUploadFileSizeMB": 20,
"defaultLetterheadId": null,
"defaultTeamId": null,
"defaultDomainId": null,
"crossTenantAccess": true,
"oauthDynamicRegistration": false,
"allowQuerySharing": false,
"aiEnabled": false,
"defaultAiProfileId": "00000000-0000-4000-8000-000000000002",
"aiSettings": {},
"excelExportSettings": {
"locked": {},
"headers": {
"showFilters": true,
"showExportName": true,
"showDescription": true,
"showInputParams": true,
"showGeneratedDate": true,
"showOrganizationName": true
},
"styling": {},
"includeHeaders": false,
"organizationName": null
},
"restrictExportTypes": false,
"allowedExportTypes": [],
"scheduleInJobTimeZone": false,
"cloudEnabled": false,
"cloudUrl": null,
"cloudToken": null,
"jwtPublicKey": null,
"frameAncestors": null,
"searchClusterId": "00000000-0000-4000-8000-000000000003",
"appDatabaseId": null,
"allowTclCommands": false,
"createdAt": "2026-01-15T16:20:00.000Z",
"updatedAt": "2026-01-15T16:20:00.000Z",
"mailIntegration": null
}
This route opens a route transaction that sets the DB role before the pre-blocks
run. Under the Tenant row-level security policy (id = CURRENT_USER), a manager
cannot resolve a different Tenant through this route (the lookup 404s). Use
the transaction-free search-cluster and app-database routes for cross-Tenant
edits.
PUT /api/tenants/{id}/search-cluster
Reassign a Tenant's Elasticsearch search cluster.
Authentication: Root manager superuser
The cluster assignment is PUT /api/tenants/{id}/search-cluster, not
GET/PUT /api/search-cluster. Clusters themselves are created and edited
through the separate /api/search-clusters collection (root manager
superuser), which has no reference page yet.
Request Body:
| Field | Type | Description |
|---|---|---|
searchClusterId | string | The search cluster to assign (payload is stripUnknown) |
A single transaction-free Tenant.update.
Example Request:
{ "searchClusterId": "00000000-0000-4000-8000-000000000001" }
Response:
Responds 200 with an empty body.
{
"searchClusterId": "00000000-0000-4000-8000-000000000001"
}
PUT /api/tenants/{id}/app-database
Reassign a Tenant's App database.
Authentication: Root manager superuser
Request Body:
| Field | Type | Description |
|---|---|---|
appDatabaseId | string (UUID) | null | The App Database profile to assign; null unassigns and falls back to the deployment default |
Existing App data is not migrated. The route runs without a transaction so
the tenant.lookup is not trapped under RLS.
Example Request:
{ "appDatabaseId": null }
Response:
Responds 200 with a null body.
{
"appDatabaseId": null
}
DELETE /api/tenants/{id}/job-history
Purge every job-history row for a Tenant.
Authentication: Superuser
This is a purge (DELETE /api/tenants/{id}/job-history), not a list read of
GET /api/job-history.
Pre-blocks: auth.assertSuperuser, tenant.lookup
Response:
Deletes every JobHistory row for the Tenant and responds 200 with an empty
body. The example below is captured as a Tenant purging its own history.
Like the update route, this opens a route transaction, so a manager cannot purge
a different Tenant's history through it (the lookup 404s under the Tenant
RLS policy id = CURRENT_USER).
DELETE /api/tenants/{id}
Delete a Tenant and cascade-delete all of its data.
Authentication: Root manager superuser
Pre-blocks: auth.assertRootManagerSuperuser, tenant.lookup(params.id)
Response:
Responds 200 with an empty body (Informer's db.remove convention), not
204.
Deleting a Tenant cascades to all of its data. The id manager is rejected by
Joi (forbidden) and cannot be deleted.
Deprecated: per-principal AI access
AI access and monthly credit caps are governed by Usage Profiles. These routes stay registered so existing scripts keep working, but they no longer read or change anything. Move integrations to the Usage Profiles routes.
All three require Superuser on the caller's own Tenant
(permission.tenant.superuser). Unknown body fields are stripped.
| Route | Accepted body | Response |
|---|---|---|
GET /api/ai-access | None | 200 with an empty collection (total: 0). |
PUT /api/ai-access/_upsert | principals (string or string[]), monthlyCostLimit (number, 0 or more) | 200 with an empty body. Nothing is stored. |
PUT /api/ai-access/_revoke | principals (string or string[]) | 200 with an empty body. Nothing is changed. |
Endpoints documented without captured examples
The following routes move binary content, call live external services, or are otherwise non-deterministic, so they are documented here without captured request/response examples.
POST /api/settings/mail/_ping
Send a test email to verify the SMTP configuration. Superuser. Reaches a live mail server, so it is not captured.
POST /api/settings/_cloud-ping
Verify connectivity to the configured Informer Cloud endpoint. Superuser. Reaches a live external service, so it is not captured.
DELETE /api/settings/mail/connection
Clear the Tenant's stored mail-integration connection. Superuser. Not captured.
POST /api/settings/apps/resources/_preview
Preview an App resource by fetching its bytes and computing an SRI hash before it is saved. Superuser. Performs a live CDN fetch, so it is not captured.
POST /api/settings/apps/resources/{id}/_refresh-sri
Re-fetch a resource's URLs and recompute their SRI hashes. Superuser. Performs a live CDN fetch, so it is not captured.
Response: { resource, results }, where results carries scripts and
styles only (assets are never re-fetched because they cannot be SRI-protected
through tag injection). Each row is { url, status } with status one of
updated (adds integrity and size), skipped (the entry has
skipIntegrity: true), or error (adds message). A failed re-fetch keeps the
previously stored hash and flags the entry sriError: true; a later success
clears the flag. capturedAt/capturedBy advance only when at least one hash
actually changed, so a wholly failed refresh does not read as a fresh capture.
GET /api/orphan-indices
List Elasticsearch indices with no corresponding Dataset. Superuser. Reaches a live Elasticsearch cluster, so it is not captured.
POST /api/_purge-indices
Delete the orphaned Elasticsearch indices. Superuser. Reaches a live Elasticsearch cluster, so it is not captured.
POST /api/_export
Export the Tenant's configuration and content as a downloadable package. Superuser.
The system export is POST /api/_export, not POST /api/tenants/{id}/_export.
This route streams a binary package, so it is not captured.
PUT/POST /api/license
Install or activate a license. Superuser. Mutates installed-license state and reaches the License Manager, so it is not captured.
POST /api/license/_sync-tenant-licenses
Sync every requested Tenant's license with the License Manager (root manager superuser). A cross-Tenant live-external operation, so it is not captured.
Request Body: tenants (required): an array of Tenant ids, or true for
every Tenant on this installation. 400 when none of them exist.
How each Tenant syncs: one batch request, made as the root manager, covers
the Tenants whose License Manager account is the manager's account or a direct
child of it, and asks the License Manager to activate them. Every other Tenant
syncs under its own license, against the same License Manager endpoint as
POST /api/license/_sync, without asking for activation. A Tenant keeps its
stored activation token unless the License Manager issues a new one.
Response: { results }, one row per requested Tenant. Success rows are
{ systemId, tenant, ok: true, activated }, where activated reports whether
the Tenant holds an activation token after the sync (a licensed-but-unactivated
Tenant is not the same as an activated one). Failure rows are
{ systemId, tenant, ok: false, error }: for example a Tenant with no license
of its own, one the License Manager does not recognize, a License Manager copy
of the license that is expired (the Tenant keeps its current license), or a
requested id that does not exist. Up to four Tenants sync at a time, each in
its own transaction, so one failure does not roll back the others. Once the
License Manager stops answering, Tenants that sync under their own license and
have not started yet fail without being sent, and Tenants the batch covered
still apply. The request itself fails only when the batch request does, for
example when the License Manager is unreachable.
A Tenant paused for maintenance is neither offered to the License Manager nor
written; its row is { systemId, tenant, ok: false, paused: true, error }. A Tenant
whose maintenance state could not be read fails the same way with paused: false
and an error asking to sync it again.
POST /api/tenants/{id}/_copy
Copy content from one Tenant into another. Root manager superuser. A long-running cross-Tenant operation, so it is not captured.
POST /api/tenants/{id}/pause
Put a Tenant into maintenance while it is backed up and moved to another deployment. Root manager superuser. Its outcome depends on timing and on the work running at the moment, so it is not captured.
Request Body:
| Field | Type | Required | Description |
|---|---|---|---|
operationId | string | Yes | The caller's id for this operation, up to 200 characters. Repeating a pause with the same body is idempotent. |
expectedDurationSeconds | integer | Yes | How long the operation is expected to take, from 1 to 86400 |
safetyFactor | number | No | Multiplier applied to the expected duration for Retry-After, above 1 and at most 100; defaults to 1.5 |
message | string | No | Plain-text message for blocked clients, up to 500 characters, in place of the default |
From the moment the pause commits, new work for the Tenant is refused: its API
requests, sign-ins, sockets, App pages and Jobs answer 503 with Retry-After
(expectedDurationSeconds times safetyFactor, rounded up) and
data.code: "TENANT_PAUSED". Work that was already running finishes. While the
Tenant is paused, a root manager request that writes to it (for example
PUT /api/tenants/{id}/search-cluster, DELETE /api/tenants/{id}, a copy or bundle
deploy into it, or impersonating one of its users) answers 409 with
data.code: "TARGET_TENANT_PAUSED".
Run no upgrade, migration or token secret rotation while any Tenant is paused: they rewrite every Tenant's rows, and the paused Tenant's writes are refused.
Response: the maintenance status (see GET /api/tenants/{id}/maintenance),
with 202 while running work drains and 200 once the Tenant is ready for backup.
Errors: 400 (TENANT_NOT_PAUSABLE) for the manager Tenant. 409
(TENANT_PAUSE_CONFLICT) when another operation holds the Tenant, or when this
operationId is already pausing it with a different body. 409
(TENANT_PAUSE_COMPLETED) when this operationId already paused and resumed it.
GET /api/tenants/{id}/maintenance
Read a Tenant's maintenance status and what still blocks a backup. Root manager
superuser. Poll it after a pause until readyForBackup is true.
Response: an open Tenant answers { tenantId, state: "running", readyForBackup: false }. A paused one also carries operationId, revision,
requestedAt, pausedAt, fencedAt, expectedDurationSeconds, safetyFactor,
retryAfterSeconds, message and activity (runningJobs, requests, tasks,
unknownProcesses). Until it is ready, blocking lists the running jobs and busy
processes, plus writes: true when writes in flight held the Tenant, or fence
with the reason it could not be fenced.
state is draining while work admitted before the pause finishes, then
paused. readyForBackup turns true once the Tenant has been paused and quiet on
every server process for at least 10 seconds. From then until the unpause, the
database refuses every write to the Tenant's rows, whatever makes it. Readiness
covers Informer's PostgreSQL database, not Elasticsearch or the file store. Every
server process on the database must run a release that has these routes, and a
process that stopped without finishing keeps readyForBackup false until it is
retired.
POST /api/tenants/{id}/unpause
Resume a paused Tenant. Root manager superuser.
Request Body: operationId (the operation that paused it) and revision (from
the last status read), both required.
Response: the status of the open Tenant (state: "running"). Repeating the call
for an operation that already resumed answers the same.
Errors: 409 (TENANT_PAUSE_STALE) when the Tenant is not paused by that
operation, or is at another revision; data then carries the operationId and
revision holding it.
POST /api/tenants/{id}/_remint-app-origins
Move a Tenant's App origins onto per-Tenant subdomains, or back, and re-mint their labels into that form. Root manager superuser. Its result depends on the deployment's App origin configuration, so it is not captured.
Request Body:
| Field | Type | Required | Description |
|---|---|---|---|
retire | boolean | Yes | true deletes the superseded labels; false keeps each one as an alias that redirects to the new origin |
subdomains | boolean | No | true, the default, mints the Tenant's App origins as <slug>.<tenant>; false mints them as <tenant>-<slug> |
A <slug>.<tenant> host needs the deployment's app.tenantSubdomains setting and
a wildcard certificate for that Tenant's own subdomain, so rollout tooling moves
each Tenant once its certificate is issued. The Tenant's appSubdomains flag
records the move. Tenants that have not moved keep minting <tenant>-<slug>
labels, which the shared wildcard always covers. subdomains: false moves a Tenant
back whether or not the deployment setting is on.
Response: { changed, subdomains }, where changed lists each relabeled origin
as { appId, surface, from, to } and subdomains is the form applied.
Errors: 409 when subdomains is true or omitted and the deployment does not
set app.tenantSubdomains. 422 when the Tenant id cannot be a host name label (at
most 63 lowercase letters, digits and hyphens, not starting or ending with a
hyphen). Both are checked before anything changes.
GET /api/app-origins
List every App's origin hosts and server routes, grouped by Tenant. Superuser. Its result depends on the deployment's App origin configuration, so it is not captured.
Query Parameters: tenant, to report on that Tenant only (404 when it does
not exist).
Access: the root manager superuser on the manager Tenant sees every Tenant, or
one with tenant. Any other superuser, including the root manager while
impersonating, sees only their own Tenant, and naming another answers 403. A
caller who is not a superuser gets 403.
Response: { appsBaseUrl, tenantSubdomains, tenants }, where
tenantSubdomains is the deployment setting. Each Tenant row carries id, name,
appSubdomains, crossTenantAccess, labels and apps. labels is flat,
subdomains or mixed from the Tenant's live App hosts (mixed marks a move that
stopped part way), or null when it has none. Each App lists its id and origins
(label, url, surface of view or draft, and canonical, true for the
live host and false for a redirect alias). For the caller's own Tenant, and for
a Tenant with crossTenantAccess, each App also carries name, slug, type,
deleted, lastInvokedAt and routes (method, path). A draft's preview
origin is listed under the App being edited.