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, per-principal AI access, 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, AI access) 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) 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.

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,
"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://informer5.zendesk.com/hc/en-us",
"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,
"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.

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,
"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://informer5.zendesk.com/hc/en-us",
"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,
"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.
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.

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,
"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://informer5.zendesk.com/hc/en-us",
"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,
"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

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,
"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://informer5.zendesk.com/hc/en-us",
"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,
"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.

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). 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: permission.tenant.superuser

Response:

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.

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

Payload: empty

Round-trips to the License Manager to refresh the Tenant's license, activation token, and AI entitlement cache. On an offline install the call short-circuits with a 503 the handler swallows, so the route still answers 200 (empty) without changing anything.

Sync the licensePOST /api/license/_sync
POST /api/license/_sync round-trips to the License Manager to refresh the tenant’s license, activation token, and AI entitlement cache. On an offline install the LM call short-circuits with a 503 the handler swallows, so the route still answers 200 (empty) without changing anything.
Request body
{}
Response · 200 · no body
Captured from the API examples test suite; ids and timestamps are normalized.

GET /api/ai-access

List per-principal AI access rows.

Authentication: permission.tenant.superuser

Response:

A HAL collection of the per-principal AI access rows (principalId + monthlyCostLimit).

List per-principal AI accessGET /api/ai-access
GET /api/ai-access requires permission.tenant.superuser and returns the per-principal AI access rows (principalId + monthlyCostLimit) as a HAL collection.
Response · 200
{
"_links": {
"self": {
"href": "https://informer.example.com/api/ai-access"
}
},
"items": [],
"start": 0,
"count": 0,
"total": 0
}
Captured from the API examples test suite; ids and timestamps are normalized.

PUT /api/ai-access/_upsert

Grant or update per-principal AI access in bulk.

Authentication: permission.tenant.superuser

Request Body:

FieldTypeRequiredDescription
principalsstring[]YesPrincipal ids (a single value is coerced to an array)
monthlyCostLimitnumberYesThe monthly credit limit applied to every listed principal

Bulk-upserts AI access for the given principals, each with the same monthlyCostLimit. Note this is PUT, not POST.

Example Request:

{ "principals": ["admin"], "monthlyCostLimit": 25 }

Response:

Responds 200 with an empty body.

Grant / update per-principal AI accessPUT /api/ai-access/_upsert
PUT /api/ai-access/_upsert (note: PUT, not POST) bulk-upserts AI access for the given principals, each with the same monthlyCostLimit. Payload is { principals: [id...] (single value coerced to array), monthlyCostLimit }. Returns 200 empty.
Request body
{
"principals": [
"admin"
],
"monthlyCostLimit": 25
}
Response · 200 · no body
Captured from the API examples test suite; ids and timestamps are normalized.

PUT /api/ai-access/_revoke

Revoke per-principal AI access in bulk.

Authentication: permission.tenant.superuser

Request Body:

FieldTypeRequiredDescription
principalsstring[]YesPrincipal ids whose AI access rows to delete

Deletes the AI access rows for the given principals. A no-op for principals with no row.

Example Request:

{ "principals": ["admin"] }

Response:

Responds 200 with an empty body.

Revoke per-principal AI accessPUT /api/ai-access/_revoke
PUT /api/ai-access/_revoke deletes the AI access rows for the given principals. Payload is { principals: [id...] }. Returns 200 empty (a no-op for principals with no row).
Request body
{
"principals": [
"admin"
]
}
Response · 200 · no body
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.create (Superuser plus a multiTenant license)

Pre-blocks: permission.tenants.create, tenant.provision

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,
"mail": {},
"helpUrl": "https://informer5.zendesk.com/hc/en-us",
"helpEmail": "",
"passwordStrength": "Weak",
"queryDatasetTTL": 60,
"jobHistorySize": 200,
"sanitizeDataHtml": true,
"jobsEnabled": true,
"secureDatasetFields": true,
"logLevelSettings": {},
"queryAuditEnabled": true,
"crossTenantAccess": true,
"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).

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,
"mail": {},
"helpUrl": "https://informer5.zendesk.com/hc/en-us",
"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,
"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.

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.


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.

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 Tenant's license with the License Manager (root manager superuser). A cross-Tenant live-external operation, so it is not captured.

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.