Skip to main content

Tenant / Admin API Overview

Looking for the product view?
The Help Center explains this surface as users see it: Administration.

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 admin user on the manager Tenant). These operate across Tenants.
Two distinct ways to edit a Tenant

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.

Get a tenantGET /api/tenants/acme
GET /api/tenants/{id} resolves through tenant.lookup under the `public` scope, returning the tenant’s non-sensitive fields. Any authenticated session may read a tenant it can see.
Response · 200
{
"_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"
}
Captured from the API examples test suite; ids and timestamps are normalized.

GET /api/tenants​

List Tenants as a HAL collection under _embedded["inf:tenant"].

Authentication: Required

Query Parameters:

ParameterTypeDescription
crossTenantAccessOnlybooleanFilter 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.

List tenantsGET /api/tenants
GET /api/tenants is a HAL collection. For a non-manager caller the where clause pins it to the caller’s OWN tenant, so it 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.
Response · 200
{
"_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"
}
]
}
}
Captured from the API examples test suite; ids and timestamps are normalized.

POST /api/tenants/_id​

Suggest a unique Tenant id from a proposed name.

Authentication: Required

Request Body:

FieldTypeRequiredDescription
namestringYesThe 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" }
Suggest a tenant id from a namePOST /api/tenants/_id
POST /api/tenants/_id slugifies the proposed name and appends a numeric suffix until the id is unique across all tenants. Returns the suggested id string. Manager-only: proposing an unused id means reading every tenant’s id, so the route requires the root manager superuser.
Request body
{
"name": "New Corporation"
}
Response · 200
"new-corporation"
Captured from the API examples test suite; ids and timestamps are normalized.

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:

FieldTypeDescription
aiEnabledbooleanMaster switch for AI features on the Tenant
defaultAiProfileIdUUID | nullThe usage profile applied to users with no team-assigned profile
aiMonthlyLimitnumberTenant-wide monthly credit ceiling
aiSettingsobjectPer-feature AI configuration/instructions
Get tenant settingsGET /api/settings
GET /api/settings returns the current tenant’s broad settings object (letterhead, export options, AI/usage knobs, security policy, ...). The route is `open: true`; it eager-loads defaultTeam/defaultDomain/defaultLetterhead/mailIntegration and projects a derived `byok` boolean from the signed license.
Response · 200
{
"_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
}
Captured from the API examples test suite; ids and timestamps are normalized.

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:

FieldTypeDescription
aiEnabledbooleanEnable/disable AI for the Tenant
defaultAiProfileIdUUID | nullSet or clear the Tenant default usage profile
aiMonthlyLimitnumber (≥ 0)Tenant-wide monthly credit ceiling
aiSettingsobjectPer-feature AI configuration

Other notable Superuser-only fields (not writable by a billing manager):

FieldTypeDescription
maxJobRetriesinteger (≥ 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.
oauthDynamicRegistrationbooleanAllows 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.

Update tenant settingsPUT /api/settings
PUT /api/settings is stripUnknown: only the whitelisted fields persist (searchClusterId is intentionally NOT one of them — use PUT /api/tenants/{id}/search-cluster). Superusers may set any field; billing managers (billing.manage) may set only aiEnabled / aiSettings / aiMonthlyLimit / defaultAiProfileId. Returns the updated tenant.
Request body
{
"sessionExpireMinutes": 120,
"allowSessionBump": true
}
Response · 200
{
"_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
}
Captured from the API examples test suite; ids and timestamps are normalized.

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).

Get the default domainGET /api/settings/default-domain
GET /api/settings/default-domain returns just the tenant’s defaultDomainId (as `{ id }`). It is `open: true` and uses the `tenant` auth strategy.
Response · 200
{
"_links": {
"self": {
"href": "https://informer.example.com/api/settings/default-domain"
}
},
"id": null
}
Captured from the API examples test suite; ids and timestamps are normalized.

GET /api/settings/mail​

Read the Tenant's SMTP configuration (the mail JSON column).

Authentication: Required

Route path

Mail settings live at /api/settings/mail, not /api/mail-settings.

Response:

Get mail settingsGET /api/settings/mail
GET /api/settings/mail (NOT /api/mail-settings, which the prose claims) returns the tenant’s SMTP config (the `mail` JSON column).
Response · 200
{
"_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"
}
Captured from the API examples test suite; ids and timestamps are normalized.

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.

FieldTypeDescription
authTypestringOne of basic, oauth, gmail, microsoft-mail, none (default none). basic is SMTP username/password auth
hoststringSMTP host (default localhost)
portintegerSMTP port (default 587)
securebooleanUse TLS on connect (default false)
ignoreTLSbooleanIgnore TLS (default false)
requireTLSbooleanRequire TLS (default false)
poolbooleanUse connection pooling (default true)
maxConnectionsintegerMax connections (default 5, min 1)
maxMessagesintegerMax messages per connection (default 100, min 1)
rateLimitintegerMessages per rateDelta (optional)
rateDeltaintegerRate-limit window in ms (default 1000, min 1)
defaultFromAddressstringDefault from address
alwaysUseDefaultbooleanAlways use the default from address (default false)
authobjectDriver-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.

Update mail settingsPUT /api/settings/mail
PUT /api/settings/mail requires permission.tenant.superuser, is stripUnknown, and applies joi defaults (authType=none, host=localhost, port=587, pool=true, ...). `authType` must be one of the registered mailAuth driver ids (basic, oauth, gmail, microsoft-mail, none); `basic` is SMTP username/password auth. Returns the saved `mail` object.
Request body
{
"authType": "basic",
"host": "smtp.example.com",
"port": 587,
"secure": false,
"requireTLS": true,
"defaultFromAddress": "noreply@example.com"
}
Response · 200
{
"_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
}
Captured from the API examples test suite; ids and timestamps are normalized.

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).

Clear mail settingsDELETE /api/settings/mail
DELETE /api/settings/mail resets the tenant’s `mail` to {} and returns the cleared object. Like all Informer DELETE routes it answers 200 (never 204), and it swallows errors (always 200).
Response · 200
{
"_links": {
"self": {
"href": "https://informer.example.com/api/settings/mail"
}
}
}
Captured from the API examples test suite; ids and timestamps are normalized.

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).

Get App settingsGET /api/settings/apps
GET /api/settings/apps (the .md’s "magic-report-settings") returns the tenant’s App CDN/CSP config merged with defaults, plus a `_defaults` block the UI uses for its reset affordance. New tenants default to the tightest CSP mode (approved-only).
Response · 200
{
"_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": []
}
}
Captured from the API examples test suite; ids and timestamps are normalized.

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).

Update App settingsPUT /api/settings/apps
PUT /api/settings/apps requires permission.tenant.superuser. `cdns` and `csp` are deep-merged (partial update), `customOrigins` is replaced wholesale. Returns the merged settings (no `_defaults` block).
Request body
{
"csp": {
"mode": "any-enabled-cdn"
}
}
Response · 200
{
"_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": []
}
Captured from the API examples test suite; ids and timestamps are normalized.

GET /api/settings/apps/resources​

List the curated CDN library catalog for Apps.

Authentication: Required

Query Parameters:

ParameterTypeDescription
enabledbooleanFilter by enabled state
featuredbooleanFilter by featured flag
categorystringFilter 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).

List App resourcesGET /api/settings/apps/resources
GET /api/settings/apps/resources is the curated CDN library catalog (the .md’s "magic-report-resources"). Rows are embedded under `inf:app-resource`, sorted featured-first then by name then version. Filterable via `enabled`, `featured`, and `category` query params.
Response · 200
{
"_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"
]
}
}
Captured from the API examples test suite; ids and timestamps are normalized.

POST /api/settings/apps/resources​

Add an App resource to the catalog.

Authentication: permission.tenant.superuser

Request Body:

FieldTypeRequiredDescription
namestringYesDisplay name
versionstringYesLibrary version
slugstringNoURL-safe id; auto-derived from name when omitted
descriptionstringNoDescription
categorystringNoCatalog category
aiHintstringNoGuidance the AI uses when choosing/using the library
resourcesobjectNo{ scripts, styles, assets } URL lists
licenseKeystringNoLicense key for commercial libraries
enabledbooleanNoWhether the resource is selectable
featuredbooleanNoWhether 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.

Add an App resourcePOST /api/settings/apps/resources
POST /api/settings/apps/resources requires permission.tenant.superuser. Payload is { name (required), version (required), slug?, description?, category?, aiHint?, resources?, licenseKey?, enabled?, featured? }. (slug is auto-derived from name when omitted; the catalog enforces a unique (slug, version) per tenant, and ships a default set of common libraries such as Chart.js, D3.js, and Highcharts.) 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). Responds 200 (not 201) with the created resource; capturedBy is the caller, capturedAt is set when `resources` are supplied.
Request body
{
"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 · 200
{
"_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"
}
Captured from the API examples test suite; ids and timestamps are normalized.

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.

Get an App resourceGET /api/settings/apps/resources/00000000-0000-4000-8000-000000000001
GET /api/settings/apps/resources/{id} resolves via appResource.lookup (404 if missing). Returns the catalog row with its resource URLs + SRI hashes.
Response · 200
{
"_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"
}
Captured from the API examples test suite; ids and timestamps are normalized.

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.

Update an App resourcePUT /api/settings/apps/resources/00000000-0000-4000-8000-000000000001
PUT /api/settings/apps/resources/{id} requires permission.tenant.superuser. Any subset of the resource fields may be sent. When `resources` change, capturedAt/capturedBy are re-stamped. Returns the updated resource.
Request body
{
"description": "Simple yet flexible JavaScript charting (updated).",
"featured": false
}
Response · 200
{
"_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"
}
Captured from the API examples test suite; ids and timestamps are normalized.

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).

Delete an App resourceDELETE /api/settings/apps/resources/00000000-0000-4000-8000-000000000001
DELETE /api/settings/apps/resources/{id} requires permission.tenant.superuser and removes the catalog row. Returns 200 with { success: true } (not 204).
Response · 200
{
"_links": {
"self": {
"href": "https://informer.example.com/api/settings/apps/resources/00000000-0000-4000-8000-000000000001"
}
},
"success": true
}
Captured from the API examples test suite; ids and timestamps are normalized.

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.

Get current licenseGET /api/license
GET /api/license 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.
Response · 200
{
"_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
}
Captured from the API examples test suite; ids and timestamps are normalized.

POST /api/license/_read​

Decode a license token without installing it.

Authentication: permission.tenant.superuser

Request Body:

FieldTypeRequiredDescription
tokenstringOne of token / uploadThe license JWT to verify and decode
uploadstringOne of token / uploadAn 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" }
Read (decode) a license tokenPOST /api/license/_read
POST /api/license/_read verifies a license JWT against Entrinsik’s public key WITHOUT installing it, returning the decoded claims so the admin UI can preview a token before applying. Send either { token } or { upload } (an uploaded license file id). 400 if the token is invalid.
Request body
{
"token": "eyJhbGciOiJIUzI1NiJ9.example-token-1.signature"
}
Response · 200
{
"_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
}
Captured from the API examples test suite; ids and timestamps are normalized.

POST /api/license/_test​

Validate a license token (or uploaded file).

Authentication: permission.tenant.superuser

Request Body:

FieldTypeRequiredDescription
tokenstringOne of token / uploadThe license token to validate
uploadstringOne of token / uploadAn 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.

Test a license tokenPOST /api/license/_test
POST /api/license/_test verifies a license token (or uploaded file) without throwing the 423 a normal license check would. Returns 200 empty when the token is valid; 422 (bad data) when it is not. Used by the install UI to validate before committing.
Request body
{
"token": "eyJhbGciOiJIUzI1NiJ9.example-token-1.signature"
}
Response · 200 · no body
Captured from the API examples test suite; ids and timestamps are normalized.

POST /api/license/_sync​

Sync the Tenant's license with the License Manager.

Authentication: permission.tenant.superuser

Request Body:

FieldTypeRequiredDescription
activatebooleanNoDefault 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
passivebooleanNoDefault 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? }:

  • updated is true only when a different license was adopted, so callers can skip repaint work on a no-op sync.
  • confirmed is true when the License Manager vouched for the license this install runs: it adopted a copy, or handed back the token already stored.
  • reason is present when confirmed is false: unusable when the License Manager holds a copy that was refused, or offline when the install is configured offline and the call short-circuited with a 503 the handler swallows. In both cases nothing changes.
Sync the licensePOST /api/license/_sync
POST /api/license/_sync round-trips to the License Manager to refresh the tenant’s license and AI entitlement cache. It answers { updated } — true only when Informer adopted a license different from the one it already held — so callers can skip repaint work on a no-op sync. An empty payload defaults to activate:true, which asks the License Manager to mint an activation; read-mostly callers should send { activate: false, passive: true } instead. On an offline install the LM call short-circuits with a 503 the handler swallows, and the route answers { updated: false, confirmed: false, reason: "offline" } without changing anything.
Request body
{}
Response · 200
{
"_links": {
"self": {
"href": "https://informer.example.com/api/license/_sync"
}
},
"updated": false,
"confirmed": false,
"reason": "offline"
}
Captured from the API examples test suite; ids and timestamps are normalized.

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.

Get the password-strength settingGET /api/password-settings
GET /api/password-settings (the route, despite the prose’s "password-strength/_test") is `auth: false` and returns the tenant’s configured minimum password strength as { passwordStrength }. It is a read of policy, not a password scorer.
Response · 200
{
"_links": {
"self": {
"href": "https://informer.example.com/api/password-settings"
}
},
"passwordStrength": "Weak"
}
Captured from the API examples test suite; ids and timestamps are normalized.

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:

FieldTypeRequiredDescription
namestringYesTenant display name
licenseTokenstringYesLicense token for the new Tenant
idstringNoTenant id; slugified from name when omitted
adminPasswordstringNoInitial admin password (default 123)
adminEmailstringNoInitial admin email
progressstringNoProgress-tracking id
copyFromIdstringNoCopy content from an existing Tenant
bundleIdstringNoDeploy from a bundle template
jwtPublicKeystringNoJWT public key for authentication
activatebooleanNoAuto-activate the license (root manager superuser only; default false)
searchClusterIdstringNoElasticsearch cluster id
appDatabaseIdstring (UUID) | nullNoManager-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.

Create a tenantPOST /api/tenants
POST /api/tenants requires permission.tenants.create (superuser + a multiTenant license) and runs tenant.provision (Tenant.create + admin user + seed copy). Payload requires { name, licenseToken }; id is slugified from name when omitted, adminPassword defaults to "123". `activate: true` auto-activates only for a root manager superuser. Responds 201 with a Location header. Captured with the manager (root) injector.
Request body
{
"id": "docs-newcorp",
"name": "New Corporation",
"licenseToken": "eyJhbGciOiJIUzI1NiJ9.example-token-1.signature"
}
Response · 201
{
"_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
}
Captured from the API examples test suite; ids and timestamps are normalized.

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.

Update a tenantPUT /api/tenants/acme
PUT /api/tenants/{id} resolves the tenant (tenant.lookup) then requires permission.tenant.write (superuser), and db.update returns the updated tenant. Distinct from PUT /api/settings, which edits the caller’s own tenant by credentials. Captured here as a tenant editing ITSELF. NOTE: db.update wraps the write in a route transaction, which sets the DB role before the pre-blocks run; under the tenant table’s RLS policy (id = CURRENT_USER) a manager cannot resolve a DIFFERENT tenant through this route (the lookup 404s). Cross-tenant tenant edits that must bypass that trap use the dedicated transaction-free routes (search-cluster, app-database).
Request body
{
"name": "Acme Analytics"
}
Response · 200
{
"_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
}
Captured from the API examples test suite; ids and timestamps are normalized.
Cross-Tenant edits 404 here

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

Route path

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:

FieldTypeDescription
searchClusterIdstringThe 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.

Reassign a tenant’s search clusterPUT /api/tenants/docs-newcorp/search-cluster
PUT /api/tenants/{id}/search-cluster (NOT /api/search-cluster, which the prose claims) requires the root manager superuser. Payload is { searchClusterId } (stripUnknown). It is a single role:false Tenant.update and returns 200 with an EMPTY body.
Request body
{
"searchClusterId": "00000000-0000-4000-8000-000000000001"
}
Response · 200 · no body
Captured from the API examples test suite; ids and timestamps are normalized.

PUT /api/tenants/{id}/app-database​

Reassign a Tenant's App database.

Authentication: Root manager superuser

Request Body:

FieldTypeDescription
appDatabaseIdstring (UUID) | nullThe 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.

Reassign a tenant’s App databasePUT /api/tenants/docs-newcorp/app-database
PUT /api/tenants/{id}/app-database requires the root manager superuser. Payload is { appDatabaseId } (UUID or null; null unassigns and falls back to the deployment default). Existing app data is NOT migrated. Returns 200 with a null body. No route transaction (it would trap the tenant.lookup under RLS).
Request body
{
"appDatabaseId": null
}
Response · 200 · no body
Captured from the API examples test suite; ids and timestamps are normalized.

DELETE /api/tenants/{id}/job-history​

Purge every job-history row for a Tenant.

Authentication: Superuser

Route shape

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.

Purge a tenant’s job historyDELETE /api/tenants/acme/job-history
DELETE /api/tenants/{id}/job-history (NOT GET /api/job-history, which the prose claims) requires auth.assertSuperuser + tenant.lookup, and DELETEs every JobHistory row for that tenant. Returns 200 with an empty body. Captured here as a tenant purging its OWN history. Like the update route, it 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).
Response · 200 · no body
Captured from the API examples test suite; ids and timestamps are normalized.
Cross-Tenant purge 404s here

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.

Delete a tenantDELETE /api/tenants/docs-newcorp
DELETE /api/tenants/{id} requires the root manager superuser and destroys the tenant (cascading all its data). The id `manager` is rejected by joi (forbidden). Returns 200 with an EMPTY body (Informer convention), NOT 204 as the prose claims.
Response · 200 · no body
Captured from the API examples test suite; ids and timestamps are normalized.
Irreversible

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​

Deprecated

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.

RouteAccepted bodyResponse
GET /api/ai-accessNone200 with an empty collection (total: 0).
PUT /api/ai-access/_upsertprincipals (string or string[]), monthlyCostLimit (number, 0 or more)200 with an empty body. Nothing is stored.
PUT /api/ai-access/_revokeprincipals (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.

Route path

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:

FieldTypeRequiredDescription
operationIdstringYesThe caller's id for this operation, up to 200 characters. Repeating a pause with the same body is idempotent.
expectedDurationSecondsintegerYesHow long the operation is expected to take, from 1 to 86400
safetyFactornumberNoMultiplier applied to the expected duration for Retry-After, above 1 and at most 100; defaults to 1.5
messagestringNoPlain-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:

FieldTypeRequiredDescription
retirebooleanYestrue deletes the superseded labels; false keeps each one as an alias that redirects to the new origin
subdomainsbooleanNotrue, 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.