# Settings

URL: https://docs.hanzo.ai/docs/openapi/settings

> How an org configures each product it uses, secret fields included.

Also for this capability: **API** · [CLI](https://docs.hanzo.ai/docs/cli/settings) · [SDKs](https://docs.hanzo.ai/docs/sdks)

How an org configures each product it uses, secret fields included.

> [All API references →](https://docs.hanzo.ai/docs/openapi) · [Get started →](https://docs.hanzo.ai/docs/guides)

|                |                                        |
| -------------- | -------------------------------------- |
| **Base URL**   | `https://api.hanzo.ai`                 |
| **Operations** | 2                                      |
| **Auth**       | `Authorization: Bearer $HANZO_API_KEY` |

## Specification

> **HIP-1151 · Settings — Per-Product Org Configuration** — Draft · [read the specification →](https://github.com/hanzoai/hips/blob/7ed737180339cd05b5f2935eafe7f7b83d2449a2/HIPs/hip-1151-settings-product-configuration.md)

`/v1/settings` is how an org configures each product it uses, secret fields
included. One engine serves every product: a product is just an `(org, product)`
key, so the console drives every product's Settings tab through this single
surface with no per-product server code. It is implemented in `hanzoai/cloud` at
`apps/settings`, and its one hard property is custody: a secret value can reach
KMS or nothing, never the database.

### Motivation

Without one configuration plane, each product grows its own — its own table, its
own masking rule, its own idea of what a secret is — and the console grows a
bespoke client per product. The drift is not hypothetical: this package once
shared a surface with the observability read paths, and separating them was the
recognition that a product's config and a product's telemetry are different
questions with different stores (`apps/settings/settings.go`, "NOT
OBSERVABILITY").

### Specification

The key words MUST, MUST NOT, SHOULD, SHOULD NOT and MAY are to be interpreted as
in RFC 2119.

#### The store

One SQLite file, opened through the fleet's one opener
(`sqlpool.Open("settings", dir)`, `apps/settings/store.go:54`), holding one JSON
config document per `(org, product)` — that composite is the primary key and the
mandatory predicate on every statement. The document is bounded (64 KiB), and the
org value is stored exactly as minted, never normalized: casing or trimming
collapses distinct owners into one bucket.

#### The address

Two typed operations: `GET /v1/settings/{product}` (read, secrets masked) and
`PUT /v1/settings/{product}` (write). The product is a console catalog slug,
validated against a DNS-label shape because it becomes both a store key segment
and a KMS ref segment — that regex is the boundary guard.

One further read lives on the internal plane, not under `/v1`: the platform's own
configuration of a product (`apps/settings/fleet_rpc.go`). It answers for the
reserved platform org and takes no org argument — the plane carries no principal,
so an op with an org parameter would be a cross-tenant read available to any app
in the pod. The constant is `authz.AdminOrg`, the same predicate the admin guard
reads, so there is no second notion of "the platform" to drift. It replaces the
environment variable as the deployment knob: an operator edits it and the next
request reads it, no restart.

#### Tenancy

The org is `principal.Org` — the value the identity middleware minted from the
validated bearer owner (HIP-0026) — and MUST NOT be read from a query parameter,
body, or client header. The client chooses a product; it never supplies the org.

#### Secret custody

A secret field's VALUE lives only in KMS at
`orgs/{org}/settings/{product}/{key}`; the store keeps the non-secret JSON plus
the list of secret key NAMES, so the read path knows which fields are
set-but-masked. Reads return a mask, and a PUT that echoes the mask back means
"unchanged" — the real value is never round-tripped. With no KMS configured, a
write carrying any secret MUST be refused whole (503) rather than dropped or
persisted in the clear; a secret the body omits keeps its stored value, so a
partial write never silently clears one.

#### Money, events, observability, stage

It is free — the surface declares `cloud.Free` (`plugin/settings/main.go`). It
publishes nothing on the bus and emits nothing beyond the request span every
route gets. The stage is `ga`: it is the configuration plane of the self-service
console itself, and every product's detail view depends on it.

#### Upstream

It derives from no upstream: the store discipline and the SQLite driver are the
fleet's own.

### Rationale

The alternative to one engine is a settings table per product, which is what the
console had to assume before this surface existed. One `(org, product)` key costs
each product the ability to have a bespoke schema — config is one opaque JSON
document — and buys the platform a single masking rule, a single custody rule and
a single client. Products that need typed, validated configuration keep it in
their own stores; this plane is for the knobs a person edits in a Settings tab.

### Security Considerations

The wrong implementation is plaintext custody: a secret written into the SQLite
file makes a copy of the deployment's data directory a copy of every org's
credentials for every product. The design closes it structurally — the write path
routes secret-shaped fields to KMS or fails closed, so the table cannot hold a
value even by bug-adjacent paths like a partial write. The second exposure is the
internal-plane read: parameterizing its org would hand any co-resident app a
cross-tenant config read, which is why the org there is a constant.

## Four surfaces

| Surface  | Reaches this capability as                       | Coverage                                                                  |
| -------- | ------------------------------------------------ | ------------------------------------------------------------------------- |
| **REST** | `settings` at its own prefix                     | 2 operations                                                              |
| **CLI**  | `hanzo settings …`                               | 2 of 2                                                                    |
| **SDK**  | `SettingsApi` in every published client          | 2 methods                                                                 |
| **MCP**  | tool `settings` on `https://api.hanzo.ai/v1/mcp` | 2 operations, 0 under the document's own id — ask `describe` for the rest |

## Quickstart

```bash
export HANZO_API_KEY=sk-...   # console.hanzo.ai → API keys
```

Then the first call. This one takes arguments — the placeholders are yours to fill. `GET /v1/settings/{product}`, operation `get_settings_by_product`:

**CLI**

```bash
hanzo settings get <product>
```

**SDK**

**.ts**

```ts
import { Configuration, SettingsApi } from 'hanzoai';

const api = new SettingsApi(new Configuration({ accessToken: process.env.HANZO_API_KEY }));
const { data } = await api.getSettingsByProduct({ product: 'product' });
```

**.py**

```python
from hanzoai.cloud import ApiClient, Configuration
from hanzoai.cloud.api import SettingsApi

client = ApiClient(Configuration(access_token=os.environ["HANZO_API_KEY"]))
result = SettingsApi(client).get_settings_by_product(product='product')
```

**.go**

```go
cfg := hanzoai.NewConfiguration()
cfg.AddDefaultHeader("Authorization", "Bearer "+os.Getenv("HANZO_API_KEY"))
client := hanzoai.NewAPIClient(cfg)

resp, _, err := client.SettingsAPI.GetSettingsByProduct(context.Background()).Execute()
if err != nil {
	return err
}
```

**.rs**

```rust
use hanzo_client::apis::{configuration::Configuration, settings_api};

let mut cfg = Configuration::new();
cfg.bearer_access_token = std::env::var("HANZO_API_KEY").ok();

let result = settings_api::get_settings_by_product(&cfg, Default::default()).await?;
```

**.java**

```java
import ai.hanzo.cloud.ApiClient;
import ai.hanzo.cloud.api.SettingsApi;

ApiClient client = new ApiClient();
client.setBearerToken(System.getenv("HANZO_API_KEY"));

var result = new SettingsApi(client).getSettingsByProduct();
```

**HTTP**

```bash
curl https://api.hanzo.ai/v1/settings/<product> \
  -H "Authorization: Bearer $HANZO_API_KEY"
```

**MCP**

MCP reaches **settings** through the `settings` tool, which names its 2 operations with its own verbs — this one among them, under a name only MCP declares. `describe` explains any of them:

```bash
curl -X POST https://api.hanzo.ai/v1/mcp \
  -H "Content-Type: application/json" \
  -d '{
       "jsonrpc": "2.0",
       "id": 1,
       "method": "tools/call",
       "params": {
         "name": "describe",
         "arguments": {
           "op": "get_setting"
         }
       }
     }'
```

Answers `200` with `object` — ok.

## Endpoints

| Endpoint                                                                                            | What it does                                                                                                                                                               |
| --------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| [`GET /v1/settings/{product}`](https://docs.hanzo.ai/docs/openapi/settings/get-settings-by-product) | Reads the caller org's configuration for one product, with every secret field MASKED — only the names of the set secrets come back, never their values, which live in KMS. |
| [`PUT /v1/settings/{product}`](https://docs.hanzo.ai/docs/openapi/settings/put-settings-by-product) | Writes the caller org's configuration for one product and answers the stored result, secrets masked.                                                                       |

---

[All Hanzo APIs](https://docs.hanzo.ai/docs/openapi) · [Interactive reference](https://docs.hanzo.ai/docs/openapi/reference)
