> ## Documentation Index
> Fetch the complete documentation index at: https://docs.steward.fi/llms.txt
> Use this file to discover all available pages before exploring further.

# Tenant Config

> Control plane configuration for per-tenant policy exposure, feature flags, theming, and approval rules.

# Tenant Config (Control Plane)

The tenant control plane config lets platform operators customize the Steward experience for each tenant — which policies are editable, what the UI looks like, which features are enabled, and how approvals work.

This is the layer between a raw multi-tenant Steward deployment and a white-labeled platform-specific experience.

**Base path:** `GET/PUT /tenants/:id/config`\
**Auth:** Tenant-level (`X-Steward-Key`)

***

## Get Tenant Config

```bash theme={null}
GET /tenants/:id/config
```

```bash theme={null}
curl https://api.steward.fi/tenants/my-platform/config \
  -H "X-Steward-Key: your-tenant-key"
```

**Response:**

```json theme={null}
{
  "ok": true,
  "data": {
    "tenantId": "my-platform",
    "displayName": "My Platform",
    "policyExposure": {
      "spending-limit": "visible",
      "rate-limit": "enforced",
      "approved-addresses": "hidden"
    },
    "policyTemplates": [...],
    "secretRoutePresets": [...],
    "approvalConfig": {
      "autoExpireSeconds": 86400,
      "approvers": { "mode": "owner" },
      "webhookCallbackEnabled": true
    },
    "featureFlags": {
      "showFundingQR": true,
      "showTransactionHistory": true,
      "showSpendDashboard": true,
      "showPolicyControls": true,
      "showApprovalQueue": true,
      "showSecretManager": false,
      "enableSolana": true,
      "showChainSelector": false,
      "allowAddressExport": true
    },
    "theme": {
      "primaryColor": "#8B5CF6",
      "accentColor": "#A78BFA",
      "backgroundColor": "#0F0F0F",
      "surfaceColor": "#1A1A2E",
      "textColor": "#FAFAFA",
      "colorScheme": "dark",
      "logoUrl": "https://assets.example.com/logo.png",
      "faviconUrl": "https://assets.example.com/favicon.ico"
    }
  }
}
```

If no config has been saved, returns a default empty config (all features enabled, no policy restrictions).

Built-in defaults ship for a small set of known tenant ids. If your tenant id matches one, its preset config is used as the fallback; otherwise the default empty config applies.

***

## App Clients And Environments

Steward supports tenant-scoped app clients for grouping local, preview, staging, and production integration settings. App-client mutations require an owner/admin session with recent MFA because they change auth and browser allowlists.

```json theme={null}
{
  "appClients": [
    {
      "id": "web-prod",
      "name": "Production Web",
      "environment": "production",
      "enabled": true,
      "isDefault": true,
      "allowedOrigins": ["https://app.example.com"],
      "allowedRedirectUrls": ["https://app.example.com/auth/callback"],
      "allowedBundleIds": ["com.example.app"],
      "allowedPackageNames": ["com.example.android"],
      "loginMethods": {
        "oauth": { "google": true, "discord": false }
      }
    },
    {
      "id": "web-dev",
      "name": "Local Dev",
      "environment": "development",
      "enabled": true,
      "allowedOrigins": ["http://localhost:3000"],
      "allowedRedirectUrls": ["http://localhost:3000/auth/callback"]
    }
  ]
}
```

OAuth authorize accepts `client_id` or `clientId`. When supplied, Steward validates `redirect_uri` against that enabled app client's redirects instead of the tenant-global fallback and applies that client's OAuth login-method overrides.

Native app clients should also pin platform identifiers with `allowedBundleIds`
for iOS and `allowedPackageNames` for Android. Steward rejects wildcards,
single-label values, and malformed identifiers before persistence, so native
callback trust must be expressed as explicit bundle/package allowlists.

When a native integration supplies `X-Steward-Native-Bundle-Id` or
`X-Steward-Native-Package-Name` on app-client-bound auth requests, Steward also
accepts equivalent JSON fields: `native_bundle_id`, `native_package_name`,
`nativeBundleId`, and `nativePackageName`. It validates identifier syntax and
rejects values that are not allowlisted on the enabled app client. For OAuth
device authorization, the accepted native identifier is bound to the issued
device code and must match again during token polling. Mismatches return
`invalid_client` and do not mint a session.

These headers and JSON fields provide deterministic server-side allowlist
enforcement. They do not prove the caller is an untampered native binary; real
device attestation, signed app claims, and anti-tamper guarantees must come from
the native platform or an attestation provider.

```bash theme={null}
GET /auth/oauth/google/authorize?tenant_id=my-platform&client_id=web-prod&redirect_uri=https%3A%2F%2Fapp.example.com%2Fauth%2Fcallback
```

```bash theme={null}
curl -X POST https://api.steward.fi/auth/device/code \
  -H "Content-Type: application/json" \
  -H "X-Steward-Native-Bundle-Id: com.example.app" \
  -d '{
    "tenantId": "my-platform",
    "client_id": "ios-prod"
  }'

curl -X POST https://api.steward.fi/auth/device/token \
  -H "Content-Type: application/json" \
  -H "X-Steward-Native-Bundle-Id: com.example.app" \
  -d '{
    "grant_type": "urn:ietf:params:oauth:grant-type:device_code",
    "device_code": "stwd_dc_...",
    "client_id": "ios-prod"
  }'
```

Dedicated aliases:

```bash theme={null}
GET /tenants/:id/app-clients
PUT /tenants/:id/app-clients
POST /tenants/:id/app-clients
DELETE /tenants/:id/app-clients/:clientId
```

### App Client Secrets

App clients can also have backend-only app secrets for Privy-style REST API authentication and HMAC request signing. The app ID is stable and public in the form `<tenantId>/<clientId>`. The app secret is returned only once when rotated or created.

Use Basic Auth with the app ID as the username and the app secret as the password, plus `X-Steward-App-Id`:

```bash theme={null}
curl https://api.steward.fi/agents \
  -H "X-Steward-App-Id: my-platform/web-prod" \
  -H "Authorization: Basic $(printf 'my-platform/web-prod:stw_app_...' | base64)"
```

Secret management requires an owner/admin session with recent MFA:

```bash theme={null}
GET /tenants/:id/app-clients/:clientId/secrets
POST /tenants/:id/app-clients/:clientId/secrets
DELETE /tenants/:id/app-clients/:clientId/secrets/:secretId
```

For signed API requests, use the same app secret as the SDK
`requestSigningSecret`. Steward validates the Basic auth secret and then checks
`X-Steward-Signature` against that tenant-scoped secret, so production
deployments do not need a separate global signing secret for app-client server
integrations.

```ts theme={null}
const steward = new StewardClient({
  baseUrl: "https://api.steward.fi",
  appId: "my-platform/web-prod",
  appSecret: "stw_app_...",
  requestSigningSecret: "stw_app_...",
});
```

### Request Signing Keys

Tenants can also manage standalone encrypted request-signing keys for server
integrations that use tenant API keys, platform keys, or delegated signer
credentials instead of app-client Basic auth.

```bash theme={null}
GET /tenants/:id/request-signing-keys
POST /tenants/:id/request-signing-keys
DELETE /tenants/:id/request-signing-keys/:keyId
```

All routes require an owner/admin session with recent MFA. `POST` returns
`signingSecret` once, stores the secret encrypted with the Steward keystore, and
marks previous active keys as `retiring` for a seven-day overlap. `GET` returns
metadata only.

```ts theme={null}
const steward = new StewardClient({
  baseUrl: "https://api.steward.fi",
  tenantId: "my-platform",
  apiKey: "stw_...",
  requestSigningSecret: "stw_sig_...",
  requestSigningKeyId: "6dd98a21-fbb6-4a2d-a840-f89a527a4244"
});
```

## Security Checklist

Tenant admins can read a production-readiness checklist for API headers, HSTS,
request freshness, HMAC request signatures, browser allowlists, production app
clients, and dashboard CSP coverage.

```bash theme={null}
GET /tenants/:id/security-checklist
```

This endpoint requires an owner/admin session with recent MFA. It returns only
status metadata, never raw secrets or environment values.

```json theme={null}
{
  "ok": true,
  "data": {
    "tenantId": "my-platform",
    "generatedAt": "2026-05-29T00:00:00.000Z",
    "summary": { "pass": 5, "warning": 1, "fail": 1 },
    "items": [
      {
        "id": "authorization-signatures",
        "label": "Authorization signatures",
        "status": "fail",
        "description": "Sensitive mutating requests need enforced HMAC signatures and configured signing secrets.",
        "remediation": "Set STEWARD_REQUIRE_AUTH_SIGNATURE=true and configure STEWARD_REQUEST_SIGNING_SECRETS or rotate an app client secret."
      }
    ]
  }
}
```

## Idempotency Metrics

Tenant admins can inspect privacy-preserving idempotency counters for replay and
conflict triage:

```bash theme={null}
GET /tenants/:id/idempotency-metrics
```

The endpoint requires an owner/admin session with recent MFA. It returns only
aggregated counters for the tenant; it never returns idempotency keys, request
bodies, stored response bodies, or credential material.

```json theme={null}
{
  "ok": true,
  "data": {
    "tenantId": "my-platform",
    "generatedAt": "2026-05-30T00:00:00.000Z",
    "windowStartedAt": "2026-05-30T00:00:00.000Z",
    "lastSeenAt": "2026-05-30T00:00:01.000Z",
    "ttlMs": 86400000,
    "counters": {
      "observed": 3,
      "reserved": 1,
      "completed": 1,
      "replayed": 1,
      "conflicts": 1,
      "inFlightConflicts": 0,
      "suppressedAuthResponses": 0,
      "invalidKeys": 0,
      "storeErrors": 0,
      "skippedUnsafeContext": 0,
      "releasedOnError": 0
    }
  }
}
```

Tenant admins can export the same privacy-preserving snapshot as CSV for
operator review or archival:

```bash theme={null}
GET /tenants/:id/idempotency-metrics/export
```

```bash theme={null}
curl "https://api.steward.fi/tenants/my-platform/idempotency-metrics/export" \
  -H "Authorization: Bearer user-session-jwt" \
  -o idempotency-metrics.csv
```

The CSV has one row with timestamps, TTL, and aggregate counter columns such as
`observed`, `completed`, `replayed`, `conflicts`, and `store_errors`. It does not
include idempotency keys, request bodies, replayed responses, or credentials.

`POST` rotates the secret, returns `appSecret` once, and marks the previous active secret as `retiring` for a short overlap window. `GET` returns metadata only: secret prefix, status, timestamps, and the public app ID.

## OIDC / JWT Login Providers

Tenant OIDC providers support direct JWT exchange and enterprise authorization-code SSO. The authorization-code fields are optional; when present, Steward acts as the relying party, verifies the returned ID token, and redirects the app with a short-lived exchange code instead of putting Steward tokens in the URL.

```json theme={null}
{
  "id": "acme-sso",
  "enabled": true,
  "issuer": "https://idp.example.com",
  "audience": ["steward-api"],
  "jwksUri": "https://idp.example.com/.well-known/jwks.json",
  "clientId": "steward-dashboard",
  "clientSecretEnv": "ACME_SSO_CLIENT_SECRET",
  "authorizationUrl": "https://idp.example.com/oauth2/v1/authorize",
  "tokenUrl": "https://idp.example.com/oauth2/v1/token",
  "scopes": ["openid", "email", "profile"]
}
```

The SP-initiated SSO route requires app-side PKCE:

```bash theme={null}
GET /auth/oidc/:provider/authorize?tenant_id=my-platform&redirect_uri=https%3A%2F%2Fapp.example.com%2Fauth%2Fcallback&code_challenge=...&code_challenge_method=S256
```

The provider callback validates the OIDC state, IdP PKCE verifier, ID-token issuer/audience/algorithm/JWKS signature, and nonce. Enterprise OIDC SSO requires a verified email claim whose domain is already verified in the tenant SSO domain registry. New JIT memberships created through this auth-code path default to `viewer`. Steward then reuses `POST /auth/oauth/exchange` for the app-bound one-time code exchange.

## SSO Email Domains

Tenant admins can register verified email domains for dashboard/team SSO discovery. This is the domain-routing layer used before SAML/OIDC redirect flows.

```bash theme={null}
GET /tenants/:id/sso-domains
POST /tenants/:id/sso-domains
POST /tenants/:id/sso-domains/:domain/verify
DELETE /tenants/:id/sso-domains/:domain
```

Create a domain:

```json theme={null}
{
  "domain": "example.com",
  "ssoRequired": true
}
```

The response includes a `verificationToken`. Publish it as an exact TXT value at `_steward-sso.example.com`, then call verify. Once verified, public discovery can route email addresses to the tenant:

```bash theme={null}
POST /auth/sso/discover
```

```json theme={null}
{
  "email": "admin@example.com"
}
```

## Team Invitations

Invite-only tenants can create pending team invitations without creating a membership up front. Invitation tokens are returned once and stored hashed.

```bash theme={null}
GET /platform/tenants/:id/invitations
POST /platform/tenants/:id/invitations
DELETE /platform/tenants/:id/invitations/:invitationId
POST /user/me/tenants/:tenantId/invitations/accept
```

Create an invitation:

```json theme={null}
{
  "email": "alice@example.com",
  "role": "developer",
  "expiresInSeconds": 604800,
  "sendEmail": true
}
```

The platform response includes `{ invitation, token, emailSent }`. Steward stores only the token hash and can send the invitation through the tenant email provider when `sendEmail` is true. If you do not send through Steward, deliver `token` through your own email channel. The user accepts it from a personal session with a verified matching email. Invite-only tenants must use `POST /user/me/tenants/:tenantId/invitations/accept` with the one-time token; email-matched invites are not accepted without the token. Invitation roles can be `admin`, `developer`, `billing`, `viewer`, or `member`; `owner` cannot be granted by invitation.

## SAML Dashboard SSO

Tenant admins can configure one SAML 2.0 IdP for dashboard/team SSO. The SAML configuration is separate from domain verification: verified SSO domains decide which tenant owns an email domain, while SAML metadata and ACS routes define the tenant's service-provider integration.

```bash theme={null}
GET /tenants/:id/saml-sso
PUT /tenants/:id/saml-sso
DELETE /tenants/:id/saml-sso
GET /auth/saml/:tenantId/login
GET /auth/saml/:tenantId/metadata
POST /auth/saml/:tenantId/acs
```

`GET /tenants/:id/saml-sso` returns the current config, if any, plus generated service-provider values:

```json theme={null}
{
  "serviceProvider": {
    "spEntityId": "https://api.example.com/auth/saml/acme/metadata",
    "acsUrl": "https://api.example.com/auth/saml/acme/acs",
    "metadataUrl": "https://api.example.com/auth/saml/acme/metadata"
  }
}
```

`PUT /tenants/:id/saml-sso` requires an owner/admin session with recent MFA and stores parsed IdP settings:

```json theme={null}
{
  "enabled": true,
  "idpEntityId": "https://idp.example.com/saml",
  "idpSsoUrl": "https://idp.example.com/sso",
  "idpCertPems": ["-----BEGIN CERTIFICATE-----\n...\n-----END CERTIFICATE-----"],
  "emailAttribute": "email",
  "groupsAttribute": "groups",
  "groupRoleMappings": [
    { "group": "Engineering", "role": "developer" },
    { "group": "Finance", "role": "billing" }
  ],
  "allowJitProvisioning": false
}
```

Steward validates that the IdP SSO URL is a public `https` URL, certificate entries are PEM certificates without private key material, at most five IdP certs are configured, and JIT-created SSO memberships can only default to `viewer` unless a SAML group maps the new user to `admin`, `developer`, `billing`, `viewer`, or `member`. SAML group mappings cannot grant `owner`. The metadata route emits unsigned SP metadata with `WantAssertionsSigned=true` and stable Entity ID/ACS URLs derived from `APP_URL`, not the request host.

The SP-initiated login route mirrors OIDC auth-code SSO and requires app-side PKCE:

```bash theme={null}
GET /auth/saml/:tenantId/login?redirect_uri=https%3A%2F%2Fapp.example.com%2Fauth%2Fcallback&code_challenge=...&code_challenge_method=S256
```

Steward stores an opaque `RelayState` and `InResponseTo` request ID, redirects to the tenant IdP, then the ACS route atomically consumes the RelayState and validates the signed SAML response/assertion against the tenant IdP certs, SP Entity ID, ACS URL, request ID, assertion timestamps, configured email attribute, verified email domain, group attributes, and assertion replay table. Successful ACS returns the app to `redirect_uri` with a short-lived exchange `code`; the app redeems it through `POST /auth/oauth/exchange`.

Client IDs must match `^[a-z0-9][a-z0-9_-]{2,63}$`. Redirect URLs must be exact `https` URLs, except loopback development URLs. Origins must be exact origins; app clients cannot use wildcard origins.

***

## MFA Policy

`authAbuseConfig.mfa` controls tenant-level step-up behavior for sensitive custody operations:

```json theme={null}
{
  "authAbuseConfig": {
    "mfa": {
      "maxAgeSeconds": 120,
      "maxAgeFor": {
        "vaultSigning": 120,
        "keyImport": 30,
        "keyExport": 300,
        "recoveryCodes": 120,
        "tenantAdmin": 120
      },
      "requireFor": {
        "vaultSigning": true,
        "keyImport": true,
        "keyExport": true,
        "recoveryCodes": true,
        "tenantAdmin": true
      },
      "disableFor": {
        "keyImport": false,
        "keyExport": false
      },
      "allowDelegatedSignerAutomation": false,
      "allowKeyQuorumAutomation": false
    }
  }
}
```

`maxAgeSeconds` is bounded from 30 seconds to 1 hour and acts as the default freshness window. `maxAgeFor` can override that window per sensitive surface with the same bounds; omitted actions fall back to `maxAgeSeconds`. Each `requireFor` flag defaults to `true`; setting a flag to `false` disables only that action's MFA freshness gate. `disableFor.keyImport` and `disableFor.keyExport` default to `false`; setting either to `true` blocks private-key import or export at the tenant-policy layer even when the corresponding environment break-glass flags are enabled and the caller has fresh MFA. When signer or key-quorum automation is disabled, vault signing routes require an owner/admin session with recent MFA instead of signer-bound API credentials. Vault signing, key import/export, user-wallet import/export, and tenant-admin control-plane routes use the tenant MFA freshness window and per-action `requireFor` flags. TOTP, SMS, recovery-code, and passkey MFA step-up sessions can satisfy recent-MFA checks; passkey step-up reuses the signed-in user's enrolled WebAuthn credentials.

***

## Update Tenant Config

```bash theme={null}
PUT /tenants/:id/config
```

Full replace — any fields omitted default to their zero values.

```bash theme={null}
curl -X PUT https://api.steward.fi/tenants/my-platform/config \
  -H "X-Steward-Key: your-tenant-key" \
  -H "Content-Type: application/json" \
  -d '{
    "displayName": "My Platform",
    "policyExposure": {
      "spending-limit": "visible",
      "rate-limit": "enforced",
      "approved-addresses": "hidden",
      "auto-approve-threshold": "visible",
      "time-window": "hidden"
    },
    "approvalConfig": {
      "autoExpireSeconds": 86400,
      "approvers": { "mode": "owner" },
      "webhookCallbackEnabled": true
    },
    "featureFlags": {
      "showFundingQR": true,
      "showTransactionHistory": true,
      "showSpendDashboard": true,
      "showPolicyControls": true,
      "showApprovalQueue": true,
      "showSecretManager": false,
      "enableSolana": true,
      "showChainSelector": false,
      "allowAddressExport": true
    },
    "theme": {
      "primaryColor": "#FF6B35",
      "colorScheme": "dark",
      "logoUrl": "https://assets.example.com/logo.png",
      "faviconUrl": "https://assets.example.com/favicon.ico"
    }
  }'
```

***

## Policy Exposure

`policyExposure` controls which policy types end users can see and edit in the `<PolicyControls>` component:

| Value        | Behavior                                          |
| ------------ | ------------------------------------------------- |
| `"visible"`  | Policy is shown and editable                      |
| `"enforced"` | Policy is applied but hidden from end users       |
| `"hidden"`   | Policy is not shown and not automatically applied |

```json theme={null}
{
  "policyExposure": {
    "spending-limit": "visible",
    "rate-limit": "enforced",
    "approved-addresses": "hidden",
    "auto-approve-threshold": "visible",
    "time-window": "hidden"
  }
}
```

***

## Policy Templates

Policy templates appear in the `<PolicyControls>` component as one-click starting points. Define them with customizable fields so users can tune limits without editing raw policy configs:

```json theme={null}
{
  "policyTemplates": [
    {
      "id": "trading-agent",
      "name": "Trading Agent",
      "description": "For agents that trade on DEXs. Spending limits + approved routers.",
      "icon": "chart-line",
      "policies": [
        {
          "id": "tpl-spend",
          "type": "spending-limit",
          "enabled": true,
          "config": {
            "maxPerTx": "100000000000000000",
            "maxPerDay": "1000000000000000000",
            "maxPerWeek": "5000000000000000000"
          }
        }
      ],
      "customizableFields": [
        {
          "path": "spending-limit.maxPerDay",
          "label": "Daily Spending Limit",
          "type": "currency",
          "default": "1.0",
          "min": "0.01",
          "max": "100"
        }
      ]
    }
  ]
}
```

***

## Secret Route Presets

Pre-configured route templates shown in the secret manager UI. Saves users from manually entering host/inject details for common APIs:

```json theme={null}
{
  "secretRoutePresets": [
    {
      "id": "openai",
      "name": "OpenAI API",
      "hostPattern": "api.openai.com",
      "pathPattern": "/*",
      "injectAs": "bearer",
      "injectKey": "Authorization",
      "injectFormat": "Bearer {value}",
      "provisioning": "platform"
    },
    {
      "id": "anthropic",
      "name": "Anthropic API",
      "hostPattern": "api.anthropic.com",
      "pathPattern": "/*",
      "injectAs": "header",
      "injectKey": "x-api-key",
      "injectFormat": "{value}",
      "provisioning": "user"
    }
  ]
}
```

`provisioning: "platform"` means the platform provides the credential. `provisioning: "user"` means the end user provides their own key.

***

## Approval Config

Controls how the approval workflow behaves for this tenant:

```json theme={null}
{
  "approvalConfig": {
    "autoExpireSeconds": 86400,
    "approvers": { "mode": "owner" },
    "webhookCallbackEnabled": true
  }
}
```

| Field                    | Description                                                    |
| ------------------------ | -------------------------------------------------------------- |
| `autoExpireSeconds`      | How long pending approvals last before auto-expiry (0 = never) |
| `approvers.mode`         | Who can approve: `"owner"` (tenant admins), `"tenant-admin"`   |
| `webhookCallbackEnabled` | Whether to fire `tx.pending` webhooks for this tenant          |

***

## Feature Flags

Toggle UI features in `@stwd/react` components:

| Flag                     | Default | Description                              |
| ------------------------ | ------- | ---------------------------------------- |
| `showFundingQR`          | `true`  | Show QR code in `<WalletOverview>`       |
| `showTransactionHistory` | `true`  | Enable `<TransactionHistory>` component  |
| `showSpendDashboard`     | `true`  | Enable `<SpendDashboard>` component      |
| `showPolicyControls`     | `true`  | Enable `<PolicyControls>` component      |
| `showApprovalQueue`      | `true`  | Enable `<ApprovalQueue>` component       |
| `showSecretManager`      | `false` | Show secret management UI                |
| `enableSolana`           | `true`  | Show Solana address alongside EVM        |
| `showChainSelector`      | `false` | Show chain-switching UI                  |
| `allowAddressExport`     | `true`  | Allow copying/exporting wallet addresses |

***

## Theme Config

Full color and typography control:

```json theme={null}
{
  "theme": {
    "primaryColor": "#8B5CF6",
    "accentColor": "#A78BFA",
    "backgroundColor": "#0F0F0F",
    "surfaceColor": "#1A1A2E",
    "textColor": "#FAFAFA",
    "mutedColor": "#6B7280",
    "successColor": "#10B981",
    "errorColor": "#EF4444",
    "warningColor": "#F59E0B",
    "borderRadius": 12,
    "fontFamily": "Inter, system-ui, sans-serif",
    "colorScheme": "dark",
    "logoUrl": "https://assets.example.com/logo.png",
    "faviconUrl": "https://assets.example.com/favicon.ico"
  }
}
```

These map directly to the CSS custom properties in `@stwd/react`. Any fields not provided fall back to the component library defaults.

Theme updates are validated before persistence: color tokens must be exact 6-digit hex values, `borderRadius` must be between `0` and `32`, `colorScheme` must be `light`, `dark`, or `system`, and `fontFamily` is restricted to common CSS font-family characters.

`logoUrl` and `faviconUrl` must be absolute HTTPS URLs. Local development URLs using `http://localhost`, `http://127.0.0.1`, or `*.localhost` are accepted; `javascript:`, `data:`, `blob:`, `file:`, relative URLs, protocol-relative URLs, non-local HTTP URLs, credentials in URLs, and values over 2048 characters are rejected. Logos must end in `.png`; favicons must end in `.ico` or `.png`. Submit an empty string to clear a saved asset URL.

***

## Related

* [React Components](/sdk/react-components) — `@stwd/react` consumes tenant config automatically
* [Approvals](/api-reference/approvals) — Configured by `approvalConfig`
* [Secrets Guide](/guides/secrets) — Configured by `secretRoutePresets`
* [Webhooks](/api-reference/webhooks) — Configured by `webhookCallbackEnabled`
