Skip to main content
A read-only, secret-safe API for platform configuration. It lets your backend scripts, monitoring, and AI agents read the same settings you manage under Settings — without ever exposing a secret. The same capability is served over two surfaces:
SurfaceEndpointAuth
RESTGET /api/v2/config and GET /api/v2/config/{path}Personal access token (PAT) with the api:full scope
MCPTools config.list / config.get at POST /mcpPersonal access token with the mcp:config:read scope
Both go through one authoritative read seam, so the allowlist, the permission checks, and the scope rules are identical regardless of which surface you call. There is no write surface — configuration is edited only in the admin UI.
Not the Public read APIThis is the authenticated, admin-grade configuration API. The anonymous, cacheable widget endpoint (GET /api/public/v1/config/{section}) is a different surface with a different allowlist (public_readable) — see Public read API.

What is readable

Exposure is a strict opt-in allowlist, filtered twice more at call time:
  1. Schema allowlist — a field is visible to the API only when the module that ships it declares api_readable: true in its configuration schema. There is no admin toggle; everything else simply does not exist as far as the API is concerned.
  2. Your section permission — you only see fields of sections your role lets you view (comerix.config.<section>.view). The list endpoint filters them out; reading one directly behaves like an unknown path.
  3. Scope policy — the value is resolved at a scope you are allowed to read (see Scope targeting).

Secrets are never readable

Secret configuration — encrypted fields (encrypted_string, e.g. the SMTP password) and fields marked sensitive — can never be exposed:
  • The platform rejects a schema outright that tries to combine api_readable with a secret field, so the combination cannot ship.
  • As defence in depth, the reader re-checks secrecy at call time and refuses to return a secret value even if one were ever flagged.
  • There is no reveal endpoint over the API at all; revealing an encrypted value exists only in the admin UI, gated by the section’s edit permission.
A secret path therefore responds exactly like a path that does not exist.

REST surface

Authentication

Requests are authenticated by a personal access token presented as a bearer credential on the stateless /api/v2/ firewall:
GET /api/v2/config HTTP/1.1
Host: flow.example.com
Authorization: Bearer cfp_xxxxxxxxxxxxxxxxxxxxxxxx
Three conditions must hold:
RequirementFailure
A valid, unexpired, unrevoked token (cfp_…)401
The token carries the api:full scope403
The token’s owner holds an admin role (ROLE_ADMIN)403
Mint the token under Account → Personal access tokens (/admin/account/tokens) and tick API: full access — see Your account & security › Personal access tokens. The token acts as you: the readable field list and the scope rules below are evaluated against your role and your workspace.

List readable fields

GET /api/v2/config
Returns the catalog of fields you may read — the schema allowlist intersected with your per-section view permissions. No values are returned; this is the discovery call.
curl -sS https://flow.example.com/api/v2/config \
  -H "Authorization: Bearer cfp_…"
Response (200)
{
  "fields": [
    {
      "path": "general/identity/site_name",
      "kind": "string",
      "label": "config.field.general.identity.site_name.label",
      "allowed_scopes": ["global", "organization", "tenant"]
    },
    {
      "path": "general/locale/default_timezone",
      "kind": "enum",
      "label": "config.field.general.locale.default_timezone.label",
      "allowed_scopes": ["global", "organization", "tenant"]
    },
    {
      "path": "general/locale/default_locale",
      "kind": "enum",
      "label": "config.field.general.locale.default_locale.label",
      "allowed_scopes": ["global", "organization", "tenant"]
    }
  ]
}
FieldMeaning
pathThe full slash path (<section>/<group>/<field>) — what you pass to the read endpoint.
kindThe field type: string, int, bool, enum, multiselect, color, url, duration, json, tags, rows, or feature_flag. (encrypted_string fields can never appear here.)
labelThe field’s label translation key, stable across releases — useful for matching against the admin UI.
allowed_scopesThe scope levels the field may be set at: any subset of global, organization, tenant.

Read one value

GET /api/v2/config/{path}
{path} is the full slash path from the listing — slashes included, no encoding needed:
curl -sS https://flow.example.com/api/v2/config/general/identity/site_name \
  -H "Authorization: Bearer cfp_…"
Response (200)
{
  "path": "general/identity/site_name",
  "kind": "string",
  "value": "Comerix Flow",
  "present": true,
  "source_scope": null
}
FieldMeaning
pathThe path you asked for.
kindThe field type (see the listing).
valueThe resolved, typed, JSON-encodable value. A feature flag returns its state object ({ "enabled": …, "rolloutPercent": …, "audience": …, "enabledFor": … }); null when no value is set at any scope and no schema default applies.
presenttrue when an effective value exists (i.e. value is not null).
source_scopeThe scope key the winning override lives at (e.g. "global", "tenant:019e6fb7-…") — or null when the value is the schema default (no override anywhere in the chain).
Reads are pure: nothing is written, no value row is created, and the response carries Cache-Control: no-store so configuration is never retained by a browser, proxy, or CDN cache.

Scope targeting

Values resolve along the same inheritance chain as the admin UI — Global → Organization → Workspace, most specific wins (see Settings › Scopes & inheritance). The optional ?scope= query parameter selects where in the chain to resolve:
curl -sS 'https://flow.example.com/api/v2/config/general/identity/site_name?scope=tenant:019e6fb7-3da4-76fe-b62b-dd9a3dcea5c4' \
  -H "Authorization: Bearer cfp_…"
SelectorResolves at
globalThe global level only.
organization:<uuid>The organization, inheriting from global.
tenant:<uuid>The workspace, inheriting from its organization and global.
Who may target what is decided by the scope-access policy, evaluated against the authenticated user — never against anything in the request:
  • A regular admin is pinned to their own workspace. Omitting scope resolves at your workspace; explicitly passing your own tenant:<your-workspace-uuid> also works. Requesting any other scope — global, an organization, or a different tenant — returns 403 scope_denied, never a silently downgraded answer.
  • A super-admin may target any scope. Omitting scope defaults to global.
The workspace comes from the tokenThe workspace identity is derived from the token’s owning user. There is no header or body field that can re-point a read at another workspace — a leaked token is confined to the workspace (and role) of its owner.

Errors

Errors use a compact {error, message} envelope:
HTTPerrorWhen
400invalid_scopeThe scope selector is malformed — not global, organization:<uuid>, or tenant:<uuid>.
401Missing, invalid, expired, or revoked token (plain 401, before any routing).
403The token lacks the api:full scope, or its owner is not an admin.
403scope_deniedThe scope is well-formed but not yours to read (see Scope targeting).
404not_foundThe path cannot be read — see below.
One 404, no oracleAn unknown path, a path that exists but is not declared api_readable, a secret field, and a section your role may not view all return the same 404 not_found. The API deliberately does not distinguish them, so a caller can never use it to enumerate which settings, sections, or modules exist. Use GET /api/v2/config to discover what you can read instead of probing paths.

MCP surface

The same reads are available to AI agents as tools on the MCP gateway (POST /mcp, JSON-RPC 2.0). Grant the token the mcp:config:read scope (label Config: read); the tools also require the comerix.config permission, and the per-section view ACL filters results exactly as on REST.
ToolTypeScopePurpose
config.listreadmcp:config:readList the configuration fields readable over the API (non-secret only), with their kind and label. No arguments.
config.getreadmcp:config:readRead one non-secret configuration value by path, optionally at a scope.
config.get arguments:
ArgumentRequiredMeaning
pathyesFull slash path, e.g. general/identity/site_name.
scopenoScope selector (global, organization:<uuid>, tenant:<uuid>); defaults to the caller’s own scope. Same policy as REST.
Example call:
curl -sS https://flow.example.com/mcp \
  -H "Authorization: Bearer cfp_…" \
  -H "Content-Type: application/json" \
  -d '{
    "jsonrpc": "2.0",
    "id": 1,
    "method": "tools/call",
    "params": {
      "name": "config.get",
      "arguments": { "path": "general/identity/site_name" }
    }
  }'
{
  "jsonrpc": "2.0",
  "id": 1,
  "result": {
    "content": [{ "type": "text", "text": "{\"path\":\"general/identity/site_name\",\"kind\":\"string\",\"value\":\"Comerix Flow\",\"present\":true,\"source_scope\":null}" }],
    "isError": false,
    "structuredContent": {
      "path": "general/identity/site_name",
      "kind": "string",
      "value": "Comerix Flow",
      "present": true,
      "source_scope": null
    }
  }
}
The payload fields are identical to the REST read. Failures follow the MCP convention: a malformed path/scope argument is a JSON-RPC -32602 Invalid params error, while a denied scope or an unreadable path comes back as a normal result with isError: true ("Configuration path not found." — the same non-enumerable not-found as REST). See MCP gateway › Error codes.

Permissions

RequirementRESTMCP
Token scopeapi:fullmcp:config:read
RoleROLE_ADMIN (admin panel access)comerix.config permission
Per-section filtercomerix.config.<section>.viewcomerix.config.<section>.view
The bearer token itself is managed under your account (comerix.account.tokens.manage — see Your account & security).