In a multi-tenant app, the org admin cannot open the console for every tenant. Instead, give each tenant admin a screen in your own app that manages their tenant. These calls use the token your app already has, and the tenant is always the one in the token — nothing in the request can point at another tenant.

Everything is under one route: /app/{orgCode}/tenant/{section}. Only a tenant admin (a user with full access in that tenant) can use it; everyone else gets 403.

SectionWhat it manages
settingsThe plain-value groups of Tenant settings: general (the key/values your Logic Blocks, Glyphs and agents read as params.settings), ui, notifications. (The password rule for app users is org-wide, under Org Settings.)
emailtemplatesThe tenant's own email templates (a tenant template overrides the organization's template of the same name).
featureflagsThe tenant's changes to the organization's feature flags — see Per-tenant flags.
membersWho is in the workspace and their status; approve someone waiting (PUT { user_id }), or remove or decline someone (DELETE ?user_id=). The last admin can't be removed (409).
invitationsThe invitations the workspace has sent (GET), and withdrawing one (DELETE ?id=). A used invitation can't be withdrawn (409).
workspaceThe workspace's own details (GET) and renaming it (PUT { name }, at most 80 characters).

What is not here

Secrets, connector credentials, AI keys, SSO and the tenant's email server are set in the console (or their own routes), never through this route and never through MCP. Asking for one of those groups answers 400.

Settings

CallDoes
GET /app/{orgCode}/tenant/settings[?group=general][&name=config]List this tenant's settings, optionally one group, or one setting by group and name.
PUT /app/{orgCode}/tenant/settings { "group": "general", "name": "config", "value": { ... } }Create or replace one setting. The whole value is replaced. POST does the same.
DELETE /app/{orgCode}/tenant/settings?group=ui&name=themeDelete one setting.
ts
// set the tenant's company name and currency; Logic Blocks read them as {{params.settings.company}}
await api.put(`/app/${org}/tenant/settings`, {
  group: 'general', name: 'config', value: { company: 'Acme', currency: 'USD' },
});

The general group's setting named config is the one your Logic Blocks, Glyphs and agents see: {{params.settings.company}}, ctx.params.settings.company. Keep a single general entry: the assets read one, and a second is not merged.

Email templates

ts
await api.put(`/app/${org}/tenant/emailtemplates`, {
  name: 'welcome',
  value: { subject: 'Welcome, {{name}}', html: '<p>Hello {{name}}</p>', text: 'Hello {{name}}' },
});
const { settings } = await api.get(`/app/${org}/tenant/emailtemplates`);

A template needs a subject and an html body. The name is the join key the email connector's template input and system emails use.

From an AI assistant (MCP)

ToolDoes
list_tenant_settings, set_tenant_setting, delete_tenant_settingThe same settings, for a tenant you name.
list_tenant_email_templates, set_tenant_email_template, delete_tenant_email_templateThe tenant's own templates.
list_tenant_feature_flags, set_tenant_feature_flag, clear_tenant_feature_flag_overrideThe tenant's flag changes.

These need a tenant. If your org has more than one, the assistant asks which tenant you mean (it can list them with list_org_tenants). For all tenants at once use the org-level tools, such as set_email_template.

Good to know

SituationWhat you get
A user who is not a tenant admin403 "Tenant admin (wildcard ACL) required"
An unknown section404 with the list of sections
A group outside the section400 naming the groups that are allowed
A value that is not an object, or too large400
ChangesTake effect for the next run — Logic Blocks, Glyphs and agents read settings when they start.