Docs state the two API keys cloud mints, and the build proves it

docs.hanzo.ai documented three key types. Cloud mints two.
cloud.APIKeyPrefixes is pk- and sk-; hk- and hz- have never existed and
an unrecognized spelling is refused at the door, so a reader's first call
failed asking for a credential nobody can issue. 149 files carried one.

sk- was the subtler half: present, and described as an org-level provider
credential when it resolves to the USER. A find-and-replace would have
left that confidently wrong, so the pages that made a further claim -- a
scope, a third family, a list of recognized prefixes -- were rewritten
rather than swapped.

Derived, not restated. openapi-doc gains Document.keys, read out of the
/v1/keys prose the Go handler writes and zipdoc lifts into the document.
gen-key-types renders that as an MDX fragment api-keys.mdx includes, so
the table nobody may type is not typed. secretKey(doc) replaces the
literal three generators each kept -- which is how three GENERATED pages
also came to teach hk-.

check-keys then holds every page to it, both directions: a prefix the
document does not carry fails the build, and a class api-keys.mdx does
not teach fails it too. Renaming pk- in cloud now stops the build in six
places instead of publishing a stale page.

Also measured against the live API and cluster, and corrected: /v1/keys
does mint keys over HTTP (403 unauthenticated, not absent); Tasks is
/v1/tasks on the one host, not gRPC elsewhere; the KMS example path
404s; and four KMSSecret manifests could not apply -- two named groups
with no controller, and their spec was External Secrets' shape, not the
CRD's five fields.

Sidebar sits flush left. The docs grid was five columns centring a band,
with the sidebar area spanning the leading gutter, so the nav began
176.5px in at 1920 and 496.5px in at 2560 -- inside its own bordered
card. Three columns now: rails pinned, page between them. Measured at
/docs/api-keys before -> after: 1920 176.5/232/1052/268/176.5 ->
232/1405/268, 2560 496.5/232/1052/268/496.5 -> 232/2045/268, nav at x=0.
Phone 390 and tablet 820 unchanged (0/375/0 and 232/573/0, drawer opens,
no page overflow); desktop collapse and restore verified.
This commit is contained in:
2026-08-13 17:48:25 -07:00
parent 38dd070943
commit f3910bbe58
159 changed files with 1034 additions and 379 deletions
+63
View File
@@ -137,6 +137,14 @@ at docs.hanzo.ai/docs/contributing/docs-architecture). Summary:
travels here unaltered); SDK reference from the ZAP SDK generator into
`content/docs/sdks/<lang>/`. Every reader of the document goes through
`scripts/openapi-doc.ts`, which owns the path — no script names the file.
A generated MDX **fragment** is the third shape here, for a fact an authored
page needs mid-sentence: `scripts/gen-key-types.ts` writes
`generated/key-types.mdx` and `content/docs/api-keys.mdx` pulls it in with
`<include>`. It sits outside `content/` because source.config globs
`**/*.mdx` there and would give a fragment its own URL — and the path is
relative to the INCLUDING file, so from `content/docs/` it is
`../../generated/`. One directory short renders the page body empty with
the frontmatter and chrome intact, which looks like a working page.
3. **Ported** (upstream OSS, mirrored with attribution, `.gitignore`d) →
`scripts/sync-project-docs.ts` into `content/docs/projects/<upstream>/`.
Port, don't re-author. Carry upstream LICENSE + NOTICE. GPL stays GPL.
@@ -161,6 +169,34 @@ zt-docs) predate the unified `content/docs/` model — migrate their content int
`content/docs/<section>/` (or a `hanzo-docs/<team>` submodule) then archive the
app.
## Two build guards, and why prose needs them
`scripts/pre-build.ts` ends with two checks that fail the build. Both exist for
the same reason: a generated page cannot be wrong about the API, an authored one
can, and a reader cannot tell which kind of page they are on.
- **`check-endpoints.ts`** — no page may name a `/v1/…` route on `api.hanzo.ai`
that the document does not serve.
- **`check-keys.ts`** — no page may spell an API key a way cloud does not mint,
and `api-keys.mdx` must teach every class the document carries. Both
directions, so renaming, adding or removing a key class in cloud stops the
build here instead of publishing a stale page.
The key classes come from `Document.keys` (`openapi-doc.ts`), read out of the
`/v1/keys` prose — cloud's own Go doc comments, lifted by zipdoc. `secretKey(doc)`
is the one accessor generators use for "the key a server presents"; three of them
had it as a literal, which is how three GENERATED pages came to teach `hk-`.
**What was wrong:** docs.hanzo.ai documented three key types — `hk-` "API Key",
`sk-` "Secret Key", `hz-` "Widget Key". Cloud mints two (`cloud.APIKeyPrefixes`:
`pk-`, `sk-`) and refuses anything else, so a reader's first call failed asking
for a key nobody can issue. `hk-`/`hz-` were on 149 files. Note the shape of the
mistake: `sk-` was *present* and *described wrongly* (as an org-level provider
credential; it resolves to the USER), so a find-and-replace would have left the
page confidently wrong. Cloud's `apps/platform/secretshape.go` still lists `hk-`
in its secret-DETECTION table — harmless, it only scans, but it is where the
invention came from.
## Branch Convention
- **`main`** — Production branch. docs.hanzo.ai is built from here and served
@@ -290,6 +326,33 @@ import { DocsPage, DocsBody } from '@hanzo/docs-ui/layouts/docs/page';
import defaultMdxComponents from '@hanzo/docs-ui/mdx';
```
### The docs grid is three columns
`layouts/docs/slots/container.tsx` (both UI packages — they are parallel forks,
so a change to one without the other diverges them): `var(--fd-sidebar-col)
minmax(0, 1fr) var(--fd-toc-width)`. Rails pin to the edges, the page takes what
is between them.
It was five columns, with `minmax(min-content, 1fr)` gutters centring a band
capped at `--fd-layout-width`, and the sidebar AREA spanned the leading gutter as
well as its own column. Measured on the live site at `/docs/api-keys`: at 1920
the columns were `176.5 232 1052 268 176.5` and the nav — 232px, right-aligned in
its area by `items-end` — began 176px from the left edge; at 2560 it began 496px
in. The empty strip was inside the sidebar's own bordered, filled card, so every
pixel a wider display gained went there. Now `232 1405 268` and `232 2045 268`,
nav at x=0.
`--fd-layout-width` no longer bounds this grid. It was doing two jobs at once —
bounding the reading measure and bounding the band — and the page slot already
owns the first (`max-w-[900px]`), so what was left was only the job that made the
gap. The article therefore stays 900px and does not move; what moved is the nav
(flush left) and the toc (flush right), and the page COLUMN absorbed the gutters.
Widening the article past 900px is a separate typography decision, untaken.
Narrow viewports are unchanged by construction: below `md`, `--fd-sidebar-width`
is 0 and the sidebar is a fixed drawer outside the grid, and `--fd-toc-width` is
0 until a toc exists.
### Layouts
- `@hanzo/docs-ui/layouts/docs` - Standard docs layout
- `@hanzo/docs-ui/layouts/home` - Homepage layout
+5
View File
@@ -37,3 +37,8 @@ content/docs/mcp-tools/
# read from /v1/pricing; the snapshot it renders from IS tracked
# (openapi-specs/pricing.json), so a builder with no egress still gets the page.
content/docs/pricing.mdx
# Generated MDX fragments that pages <include> — see scripts/gen-key-types.ts.
# Not under content/, because source.config globs `**/*.mdx` there and would
# give a fragment its own URL.
generated/
+1 -1
View File
@@ -65,7 +65,7 @@ const DOORS = [
external: false,
cta: 'Read the API reference',
lang: 'Request',
code: 'curl https://api.hanzo.ai/v1/chat/completions \\\n -H "Authorization: Bearer hk-..." \\\n -d \'{"model":"enso","messages":[...]}\'',
code: 'curl https://api.hanzo.ai/v1/chat/completions \\\n -H "Authorization: Bearer sk-..." \\\n -d \'{"model":"enso","messages":[...]}\'',
},
] as const;
+2 -2
View File
@@ -10,7 +10,7 @@ Build, deploy, and run autonomous agents: configured personas with a model, a to
## Quickstart
Define an agent and run it with the SDK. The same `hk-*` key authenticates every call.
Define an agent and run it with the SDK. The same `sk-*` key authenticates every call.
```typescript
import { Agent, Runtime } from '@hanzo/agents'
@@ -44,7 +44,7 @@ Agents also run over REST through the gateway. Point the SDK at `https://api.han
```bash
curl -X POST https://api.hanzo.ai/v1/agents/runs \
-H "Authorization: Bearer hk-..." \
-H "Authorization: Bearer sk-..." \
-H "Content-Type: application/json" \
-d '{
"agent": "researcher",
+146 -92
View File
@@ -1,133 +1,159 @@
---
icon: KeyRound
title: API Keys
description: Key types, scopes, creation, rotation, and production best practices for Hanzo API keys.
description: The two Hanzo key types, which one may ship in a browser, and how to create, rotate and revoke them.
---
# API Keys
Every request to the Hanzo platform is authenticated with a bearer token. This page covers the three key types, how to create and use them, and how to keep them safe.
Every request to the Hanzo platform is authenticated with a bearer token. There
are two kinds of key, they are not interchangeable, and the difference is what
each one resolves to when the door reads it.
## Key Types and Prefixes
## The two types
Hanzo uses key prefixes so you can identify a credential at a glance and catch accidental misuse.
<include>../../generated/key-types.mdx</include>
| Prefix | Type | Where to Use | Scope |
|--------|------|--------------|-------|
| `hk-*` | **API Key** | Server-side code, CI/CD, scripts | Project-level. Full access to APIs within the project's permissions. |
| `sk-*` | **Secret Key** | Backend services, provider configuration | Org-level. Used for upstream provider credentials and internal service auth. Never expose to clients. |
| `hz-*` | **Widget Key** | Browser JavaScript, mobile apps | Project-level with restricted scopes. Safe to embed in client-side code. |
### What each one resolves to
### API Keys (`hk-*`)
A **secret key** resolves to **you**. The door looks it up and gets your user
row, which makes it session-equivalent: anything you can do in the console, a
holder of your `sk-` can do over HTTP. That is why it belongs on a server and
nowhere else.
These are your primary credentials. One `hk-*` key gives access to all Hanzo services for the project it belongs to: AI API, search, storage, tasks, and everything else.
A **publishable key** resolves to **your organization** and stops there — it
never yields a principal, so it cannot read anything. It identifies; it does not
authenticate. That is the property that makes it safe to ship in a browser
bundle, and it is enforced at the door rather than promised: a `pk-` presented
as a bearer credential is refused outright, no matter which endpoint it arrives
at. One `pk-` covers analytics, product insights and error capture, which is why
you do not need a separate key or a separate DSN for each.
```bash
# All Hanzo API calls use the same header format
curl -H "Authorization: Bearer hk-proj-abc123..." https://api.hanzo.ai/v1/models
```
A publishable key minted alongside a project names that project rather than the
organization, so beacons from two sites attribute separately. It is the same
spelling and the same rules.
### Secret Keys (`sk-*`)
### They are two rows, not two halves of one key
Secret keys are org-scoped credentials used for provider-level operations: configuring upstream LLM providers, managing billing integrations, and inter-service communication. They carry elevated privileges and must never leave your backend.
A user holds at most one key of each type, and IAM stores them as separate rows.
So rotating the key in your browser bundle does not sign your servers out, and
rotating your server key does not blank the analytics on your site. They fail
independently, which is the point of having two.
### Widget Keys (`hz-*`)
Asking for a type that does not exist is an error, not a default. The API will
not hand you a secret key because you asked for something it did not recognize —
handing an `sk-` to someone who wanted a browser key is exactly the accident the
refusal exists to prevent.
Widget keys are designed for client-side use. They have restricted scopes -- typically limited to chat completions and embeddings -- so that exposing them in browser code does not compromise your account.
## Creating keys
```html
<!-- Safe to embed in frontend code -->
<script>
const HANZO_KEY = 'hz-widget-abc123...'
</script>
```
## Creating Keys
### In the Console
### In the console
1. Go to [console.hanzo.ai](https://console.hanzo.ai)
2. Select your **project**
3. Click **API Keys** in the sidebar
4. Click **Create Key**
5. Choose the key type (`hk-*` API key or `hz-*` widget key)
6. Optionally set:
- **Name** -- descriptive label (e.g. `prod-backend`, `staging-widget`)
- **Expiration** -- auto-expire after a set duration
- **Budget** -- monthly spend limit for LLM usage
- **Rate limit** -- requests per minute cap
7. Copy the key immediately -- it will not be shown again
2. Open **API Keys**
3. Create a key and choose its type
4. Copy a secret key immediately — it is shown once and never again
Key issuance, budgets, and revocation are console operations — there is no public key-minting endpoint, by design: a credential that can mint credentials is the one thing you never want reachable over HTTP with a bearer token.
A publishable key is the exception: you can read it back at any time, because it
is public by construction and useless to you if you cannot.
## Using Keys
### Over the API
All Hanzo APIs accept keys via the `Authorization` header with the `Bearer` scheme:
Keys are one resource, `/v1/keys`, and the type is a field on it. There is no
separate mint endpoint and no separate revoke endpoint — the HTTP method says
which you mean.
```
Authorization: Bearer hk-proj-abc123...
```bash
# Mint (or rotate) a key. The response carries it once.
curl -X POST https://api.hanzo.ai/v1/keys \
-H "Authorization: Bearer sk-your-key" \
-H "Content-Type: application/json" \
-d '{"type":"publishable"}'
# List what you hold. No secret material comes back — a secret key is
# represented by its prefix; a publishable key by its full value.
curl https://api.hanzo.ai/v1/keys -H "Authorization: Bearer sk-your-key"
# Revoke one type without touching the other.
curl -X DELETE "https://api.hanzo.ai/v1/keys?type=publishable" \
-H "Authorization: Bearer sk-your-key"
```
Every service answers on the one host, `api.hanzo.ai`, under `/v1/<service>` — the same key works across all of them:
Creating is how you rotate: you hold one key per type, so minting the same type
again replaces it and the old credential stops working. Omitting `type` means
`secret`, which is what every caller meant before publishable keys existed.
Two things worth knowing before you automate this. A browser session must echo
an anti-CSRF token — `GET /v1/csrf`, then send it back as `X-CSRF-Token` — while
a bearer caller does not, because a bearer request cannot be forged by a
cross-site page. And revocation reaches IAM immediately but the gateway caches
key lookups for a few minutes, so a request that beat the cache may still be
served.
The full request and response shapes are in the [Keys
reference](/docs/openapi/keys), generated from the same source as this page.
## Using keys
All Hanzo APIs take the key in the `Authorization` header with the `Bearer`
scheme:
```
Authorization: Bearer sk-your-key
```
Every service answers on one host, `api.hanzo.ai`, under `/v1/<service>` — the
same key works across all of them:
| Service | Path | Example |
|---------|------|---------|
| AI | `/v1/chat/completions`, `/v1/models`, `/v1/embeddings` | `api.hanzo.ai/v1/chat/completions` |
| Search | `/v1/search` | `api.hanzo.ai/v1/search` |
| Secrets | `/v1/kms` | `api.hanzo.ai/v1/kms/orgs/{org}/secrets` |
| Secrets | `/v1/kms` | `api.hanzo.ai/v1/kms/secrets` |
| Deploys | `/v1/platform` | `api.hanzo.ai/v1/platform/projects` |
| Analytics | `/v1/analytics` | `api.hanzo.ai/v1/analytics/overview` |
| Observability | `/v1/o11y` | `api.hanzo.ai/v1/o11y/status?product=cloud` |
| Tasks | `tasks-api.hanzo.ai` | gRPC with metadata header |
| Tasks | `/v1/tasks` | `api.hanzo.ai/v1/tasks/namespaces` |
### SDK Configuration
### SDK configuration
All official SDKs accept the API key as a constructor parameter:
All official SDKs accept the key as a constructor parameter:
```python
# Python
from hanzoai import Hanzo
client = Hanzo(api_key="hk-your-key")
client = Hanzo(api_key="sk-your-key")
```
```typescript
// TypeScript
import Hanzo from '@hanzo/ai'
const client = new Hanzo({ apiKey: 'hk-your-key' })
const client = new Hanzo({ apiKey: 'sk-your-key' })
```
```go
// Go
import "github.com/hanzoai/go-sdk"
client := hanzo.NewClient(hanzo.WithAPIKey("hk-your-key"))
client := hanzo.NewClient(hanzo.WithAPIKey("sk-your-key"))
```
Each client defaults to `https://api.hanzo.ai/v1`. A third-party HTTP client
written against the chat-completions shape works too — point its base URL there
and give it the same key.
## Scopes
## Spend and usage
### Project-Level Keys (`hk-*`, `hz-*`)
These keys are bound to a single project. They can only access resources within that project. Switching projects requires a different key.
### Org-Level Keys (`sk-*`)
These keys operate across all projects within an organization. They are used for administrative operations: managing projects, configuring providers, and accessing org-wide analytics.
## Key Information and Usage
Spend and usage for the calling credential come from the billing and analytics APIs — same host, same key:
Spend for the calling credential comes from the billing and analytics APIs —
same host, same key:
```bash
# Current balance
curl https://api.hanzo.ai/v1/billing/balance \
-H "Authorization: Bearer hk-your-key"
-H "Authorization: Bearer sk-your-key"
# Itemized metered usage
curl https://api.hanzo.ai/v1/billing/usage \
-H "Authorization: Bearer hk-your-key"
-H "Authorization: Bearer sk-your-key"
```
```json
@@ -148,44 +174,72 @@ curl https://api.hanzo.ai/v1/billing/usage \
}
```
For rolled-up request/token/spend counts across a range, use `GET /v1/analytics/overview?range=30d` — see [Analytics](/docs/services/analytics).
For rolled-up request, token and spend counts across a range, use `GET
https://api.hanzo.ai/v1/analytics/overview?range=30d` — see
[Analytics](/docs/services/analytics).
## Rotation Best Practices
## Rotation
1. **Rotate regularly** -- Set key expiration at creation time. 90 days is a reasonable default for production keys.
2. **Use KMS for production** -- Store keys in [Hanzo KMS](/docs/services/kms) and sync them to your runtime environment via KMSSecret CRDs. Never hardcode keys in source or commit them to git.
3. **Create before you revoke** -- Generate the replacement key, deploy it, verify it works, then revoke the old one. Zero-downtime rotation.
4. **One key per service** -- Give each service or environment its own key. This limits blast radius and makes audit logs useful.
5. **Set budgets** -- Every production key should have a monthly budget. This prevents runaway costs from bugs or abuse.
6. **Monitor usage** -- Check `/v1/billing/usage` or the console dashboard regularly. Unusual spend patterns may indicate a leak.
1. **Rotate on a schedule.** Ninety days is a reasonable default for a secret
key. A publishable key is public anyway; rotate it when you want to cut off a
site, not on a calendar.
2. **Keep production keys in [KMS](/docs/services/kms).** Never in source, never
in a committed `.env`.
3. **Mint before you revoke.** Mint the replacement, deploy it, watch it work,
then revoke. Note that minting the same type again IS the revoke — so for
zero-downtime you cut over between two identities, not two keys of one.
4. **One identity per service.** Blast radius, and audit logs you can read.
5. **Watch spend.** `/v1/billing/usage` or the console. An unfamiliar spend
pattern is what a leaked key looks like from the inside.
### KMS Integration
### KMS integration
For production deployments, use Hanzo KMS to manage API keys as secrets:
A `KMSSecret` syncs secrets out of Hanzo KMS into a Kubernetes Secret your pods
already read. It authenticates as a machine identity, names a scope in KMS, and
names the Secret it manages:
```yaml
# KMSSecret CRD -- syncs a secret from Hanzo KMS into K8s
apiVersion: secrets.lux.network/v1alpha1
kind: KMSSecret
metadata:
name: hanzo-api-key
name: api-kms-sync
namespace: acme
spec:
secretStoreRef:
name: kms-hanzo
data:
- secretKey: HANZO_API_KEY
remoteRef:
secretPath: /prod/api-keys
secretKey: primary
hostAPI: https://kms.hanzo.ai
resyncInterval: 60
authentication:
universalAuth:
credentialsRef:
secretName: acme-iam-creds
secretNamespace: acme
secretsScope:
projectSlug: acme
envSlug: prod
secretsPath: /api
keys:
- HANZO_API_KEY
managedSecretReference:
creationPolicy: Orphan
secretName: api-secrets
secretNamespace: acme
secretType: Opaque
```
## Revoking Keys
`creationPolicy: Orphan` means deleting the CR leaves the Secret in place, so
removing the sync does not take a running workload down with it.
Revoke a key from the console: **Project → API Keys → Revoke**. Revocation is immediate — any in-flight request using the key fails with `401 Unauthorized`.
## Revoking
## Security Rules
From the console: **API Keys → Revoke**. Over the API:
`DELETE /v1/keys?type=secret`. IAM drops the credential immediately and calls
made with it fail `401` once the gateway's cache lapses.
- **Never commit keys to git.** Use environment variables or KMS.
- **Never expose `hk-*` or `sk-*` keys client-side.** Use `hz-*` widget keys for browser code.
- **Always use HTTPS.** All Hanzo endpoints enforce TLS. Plain HTTP connections are rejected.
- **Set the minimum scope needed.** If a service only needs LLM access, do not give it an org-level secret key.
## Rules
- **Never commit a key.** Environment variables or KMS.
- **Never put a secret key in a browser.** It resolves to you. Use a publishable
key, which resolves to your org and cannot read.
- **Always use HTTPS.** Every Hanzo endpoint enforces TLS; plain HTTP is
rejected.
- **Ask for the type you need.** An unrecognized type is refused, so a typo
fails loudly instead of quietly handing you the more powerful key.
+4 -4
View File
@@ -29,10 +29,10 @@ Every Hanzo service exposes a REST API under one base URL. One credential works
https://api.hanzo.ai/v1
```
Authenticate every request with a bearer token in the `Authorization` header. Use an API key (`hk-...`) for server-to-server calls, or an IAM access token for user-facing apps:
Authenticate every request with a bearer token in the `Authorization` header. Use an API key (`sk-...`) for server-to-server calls, or an IAM access token for user-facing apps:
```
Authorization: Bearer hk-...
Authorization: Bearer sk-...
```
All traffic is TLS-only and passes through the [Gateway](/docs/gateway), which authenticates, rate-limits, and routes it. See [API Keys](/docs/api-keys) to create a key and [Authentication](/docs/authentication) for OIDC tokens.
@@ -52,14 +52,14 @@ The API is versioned at `v1`; it evolves additively, so integrations stay stable
```bash
curl https://api.hanzo.ai/v1/models \
-H "Authorization: Bearer hk-..."
-H "Authorization: Bearer sk-..."
```
Responses are JSON. Errors use standard HTTP status codes -- `401` for a bad or missing key, `403` when authenticated but forbidden, `429` when rate-limited, `402` when out of credits.
## Related
- [API Keys](/docs/api-keys) -- create and manage `hk-` credentials
- [API Keys](/docs/api-keys) -- create and manage `sk-` credentials
- [Authentication](/docs/authentication) -- OAuth 2.0 + OIDC for user tokens
- [Gateway](/docs/gateway) -- the entry point every request passes through
- [OpenAPI](/docs/openapi) -- machine-readable specs and generated references
+2 -2
View File
@@ -40,6 +40,6 @@ Newest builds are always on the [latest release](https://github.com/hanzoai/exte
## Sign in
Open the extension and **Sign in with Hanzo**, or paste a `hk-` key in its settings.
Open the extension and **Sign in with Hanzo**, or paste a `sk-` key in its settings.
> **One identity, everywhere.** Sign in with Hanzo ([IAM](/docs/iam) OAuth) or paste a `hk-` [API key](/docs/api-keys). The same credential works in every Hanzo surface — see [Integrations & Extensions](/docs/apps).
> **One identity, everywhere.** Sign in with Hanzo ([IAM](/docs/iam) OAuth) or paste a `sk-` [API key](/docs/api-keys). The same credential works in every Hanzo surface — see [Integrations & Extensions](/docs/apps).
+1 -1
View File
@@ -39,4 +39,4 @@ Answer store questions, summarize orders, and draft product copy.
**Connect** — [console](https://console.hanzo.ai) → **Shopify** → authorize the app for your store.
> **One identity, everywhere.** Sign in with Hanzo ([IAM](/docs/iam) OAuth) or paste a `hk-` [API key](/docs/api-keys). The same credential works in every Hanzo surface — see [Integrations & Extensions](/docs/apps).
> **One identity, everywhere.** Sign in with Hanzo ([IAM](/docs/iam) OAuth) or paste a `sk-` [API key](/docs/api-keys). The same credential works in every Hanzo surface — see [Integrations & Extensions](/docs/apps).
+1 -1
View File
@@ -41,4 +41,4 @@ Draft replies, summarize tickets, and suggest macros from the agent sidebar.
Newest installers are always on the [latest release](https://github.com/hanzoai/extension/releases/latest).
> **One identity, everywhere.** Sign in with Hanzo ([IAM](/docs/iam) OAuth) or paste a `hk-` [API key](/docs/api-keys). The same credential works in every Hanzo surface — see [Integrations & Extensions](/docs/apps).
> **One identity, everywhere.** Sign in with Hanzo ([IAM](/docs/iam) OAuth) or paste a `sk-` [API key](/docs/api-keys). The same credential works in every Hanzo surface — see [Integrations & Extensions](/docs/apps).
+2 -2
View File
@@ -31,6 +31,6 @@ Newest builds are always on the [latest release](https://github.com/hanzoai/exte
## Sign in
Open the plugin and **Sign in with Hanzo**, or paste a `hk-` key in its settings.
Open the plugin and **Sign in with Hanzo**, or paste a `sk-` key in its settings.
> **One identity, everywhere.** Sign in with Hanzo ([IAM](/docs/iam) OAuth) or paste a `hk-` [API key](/docs/api-keys). The same credential works in every Hanzo surface — see [Integrations & Extensions](/docs/apps).
> **One identity, everywhere.** Sign in with Hanzo ([IAM](/docs/iam) OAuth) or paste a `sk-` [API key](/docs/api-keys). The same credential works in every Hanzo surface — see [Integrations & Extensions](/docs/apps).
+2 -2
View File
@@ -33,11 +33,11 @@ Add Hanzo's MCP tools to Anthropic's Claude Desktop with a one-file connector.
|-------|--------|
| [`hanzo-ai-claude-v1.9.31.dxt`](https://github.com/hanzoai/extension/releases/download/v1.9.31/hanzo-ai-claude-v1.9.31.dxt) | Desktop extension (`.dxt`) |
**Install** — double-click the `.dxt` to open it with Claude Desktop, then confirm the install. Provide your `hk-` key when prompted and the Hanzo tools appear in Claude's tool list.
**Install** — double-click the `.dxt` to open it with Claude Desktop, then confirm the install. Provide your `sk-` key when prompted and the Hanzo tools appear in Claude's tool list.
Newest builds are always on the [latest release](https://github.com/hanzoai/extension/releases/latest).
> **One identity, everywhere.** Sign in with Hanzo ([IAM](/docs/iam) OAuth) or paste a `hk-` [API key](/docs/api-keys). The same credential works in every Hanzo surface — see [Integrations & Extensions](/docs/apps).
> **One identity, everywhere.** Sign in with Hanzo ([IAM](/docs/iam) OAuth) or paste a `sk-` [API key](/docs/api-keys). The same credential works in every Hanzo surface — see [Integrations & Extensions](/docs/apps).
## Related
+1 -1
View File
@@ -27,7 +27,7 @@ The same review and triage flow for GitLab merge requests and issues.
**Connect** — from the [console](https://console.hanzo.ai), choose **GitLab**, authorize over OAuth (GitLab.com or self-managed), and pick the groups or projects to enable.
> **One identity, everywhere.** Sign in with Hanzo ([IAM](/docs/iam) OAuth) or paste a `hk-` [API key](/docs/api-keys). The same credential works in every Hanzo surface — see [Integrations & Extensions](/docs/apps).
> **One identity, everywhere.** Sign in with Hanzo ([IAM](/docs/iam) OAuth) or paste a `sk-` [API key](/docs/api-keys). The same credential works in every Hanzo surface — see [Integrations & Extensions](/docs/apps).
## Related
+2 -2
View File
@@ -45,9 +45,9 @@ Restart JupyterLab; the Hanzo panel appears in the sidebar.
## Sign in
Open the Hanzo panel and **Sign in with Hanzo**, or set your `hk-` key in the extension settings (`hanzo.apiKey`).
Open the Hanzo panel and **Sign in with Hanzo**, or set your `sk-` key in the extension settings (`hanzo.apiKey`).
> **One identity, everywhere.** Sign in with Hanzo ([IAM](/docs/iam) OAuth) or paste a `hk-` [API key](/docs/api-keys). The same credential works in every Hanzo surface — see [Integrations & Extensions](/docs/apps).
> **One identity, everywhere.** Sign in with Hanzo ([IAM](/docs/iam) OAuth) or paste a `sk-` [API key](/docs/api-keys). The same credential works in every Hanzo surface — see [Integrations & Extensions](/docs/apps).
## Related
+2 -2
View File
@@ -20,7 +20,7 @@ Every surface below is a thin client over the same foundation:
You authenticate **once**, either way:
1. **Sign in with Hanzo** — OAuth through [Hanzo IAM](/docs/iam). Click **Sign in**, approve, done.
2. **Paste a `hk-` key** — create one in the [console](https://console.hanzo.ai) (see [API Keys](/docs/api-keys)) and paste it into the surface's settings.
2. **Paste a `sk-` key** — create one in the [console](https://console.hanzo.ai) (see [API Keys](/docs/api-keys)) and paste it into the surface's settings.
The same key and the same identity work in every extension, add-in, and connected app. Switch orgs once and it applies everywhere.
@@ -51,6 +51,6 @@ Connect these from the [console](https://console.hanzo.ai) or install the app in
## Related
- [Gateway](/docs/gateway) — the one API every surface calls
- [API Keys](/docs/api-keys) — mint the `hk-` key you paste in
- [API Keys](/docs/api-keys) — mint the `sk-` key you paste in
- [IAM](/docs/iam) — the identity you sign in with
- [Integrations](/docs/integrations) — connect data sources (blob storage, Slack alerts, analytics) to a project
+1 -1
View File
@@ -47,4 +47,4 @@ Summarize, query, and extract structured data from PDFs at [pdf.hanzo.ai](https:
Newest installers are always on the [latest release](https://github.com/hanzoai/extension/releases/latest).
> **One identity, everywhere.** Sign in with Hanzo ([IAM](/docs/iam) OAuth) or paste a `hk-` [API key](/docs/api-keys). The same credential works in every Hanzo surface — see [Integrations & Extensions](/docs/apps).
> **One identity, everywhere.** Sign in with Hanzo ([IAM](/docs/iam) OAuth) or paste a `sk-` [API key](/docs/api-keys). The same credential works in every Hanzo surface — see [Integrations & Extensions](/docs/apps).
+2 -2
View File
@@ -17,6 +17,6 @@ Summarize matters, draft correspondence, and query your practice data inside Cli
Run Hanzo from the Raycast command bar — ask, draft, and run agents without leaving your keyboard.
**Install** — open Raycast, search the **Raycast Store** for **Hanzo**, and add the extension. Set your `hk-` key in the extension preferences, or run the **Sign in with Hanzo** command.
**Install** — open Raycast, search the **Raycast Store** for **Hanzo**, and add the extension. Set your `sk-` key in the extension preferences, or run the **Sign in with Hanzo** command.
> **One identity, everywhere.** Sign in with Hanzo ([IAM](/docs/iam) OAuth) or paste a `hk-` [API key](/docs/api-keys). The same credential works in every Hanzo surface — see [Integrations & Extensions](/docs/apps).
> **One identity, everywhere.** Sign in with Hanzo ([IAM](/docs/iam) OAuth) or paste a `sk-` [API key](/docs/api-keys). The same credential works in every Hanzo surface — see [Integrations & Extensions](/docs/apps).
+1 -1
View File
@@ -41,4 +41,4 @@ Summarize financials and answer questions about your books.
- **Canvas** — education / LMS.
- **iManage** — legal document management.
> **One identity, everywhere.** Sign in with Hanzo ([IAM](/docs/iam) OAuth) or paste a `hk-` [API key](/docs/api-keys). The same credential works in every Hanzo surface — see [Integrations & Extensions](/docs/apps).
> **One identity, everywhere.** Sign in with Hanzo ([IAM](/docs/iam) OAuth) or paste a `sk-` [API key](/docs/api-keys). The same credential works in every Hanzo surface — see [Integrations & Extensions](/docs/apps).
+1 -1
View File
@@ -27,7 +27,7 @@ A check evaluates a `(subject, resource, action)` request against the active pol
```bash
curl -X POST https://api.hanzo.ai/v1/authz/check \
-H "Authorization: Bearer hk-..." \
-H "Authorization: Bearer sk-..." \
-H "Content-Type: application/json" \
-d '{
"subject": "user:alice",
+2 -2
View File
@@ -13,7 +13,7 @@ Each instance is an isolated Base backend scoped to your organization, with its
```bash
curl -X POST https://api.hanzo.ai/v1/base \
-H "Authorization: Bearer hk-..." \
-H "Authorization: Bearer sk-..." \
-H "X-Org-Id: org_a1b2c3" \
-H "Content-Type: application/json" \
-d '{ "name": "acme-app", "iam": true }'
@@ -59,5 +59,5 @@ Each tenant instance is fully isolated, so one organization's data and users nev
- [Hanzo Base service reference](/docs/services/base) — collections, hooks, plugins, and SDKs
- [SQL](/docs/sql) — the managed PostgreSQL that backs production instances
- [Storage](/docs/storage) — S3-compatible storage for uploaded files
- [API Keys](/docs/api-keys) — authenticate with `hk-` keys
- [API Keys](/docs/api-keys) — authenticate with `sk-` keys
- [API Reference](/docs/openapi) — every endpoint at `api.hanzo.ai`
+3 -3
View File
@@ -16,7 +16,7 @@ Your organization spends from a **cloud-credit balance**. Every billable request
```bash
# Check your organization's credit balance
curl https://api.hanzo.ai/v1/billing/balance \
-H "Authorization: Bearer hk-..."
-H "Authorization: Bearer sk-..."
```
```json
@@ -45,7 +45,7 @@ Most Hanzo products are **usage-based**: you pay for what you consume -- tokens
```bash
# Metered usage for the current period, by product and model
curl "https://api.hanzo.ai/v1/billing/usage?period=current" \
-H "Authorization: Bearer hk-..."
-H "Authorization: Bearer sk-..."
```
Prices are published in the pricing catalog, which the console's Plans & Pricing view reads live:
@@ -64,7 +64,7 @@ At the close of each billing cycle, usage and subscription charges are rolled in
```bash
# List invoices for your organization
curl https://api.hanzo.ai/v1/billing/invoices \
-H "Authorization: Bearer hk-..."
-H "Authorization: Bearer sk-..."
```
Each invoice lists line items, the period covered, credits applied, and the amount due. The console's Cost view renders the same data with per-product breakdowns.
@@ -94,7 +94,7 @@ agents:
```bash
# At least one LLM provider key
HANZO_API_KEY=hk-your-api-key
HANZO_API_KEY=sk-your-api-key
# Database
MONGO_URI=mongodb://localhost:27017/chat
+1 -1
View File
@@ -35,7 +35,7 @@ MEILI_MASTER_KEY=${SEARCH_MASTER_KEY}
```bash
# Hanzo Gateway (recommended — access all models via one key)
HANZO_API_KEY=hk-your-api-key
HANZO_API_KEY=sk-your-api-key
# Direct provider keys (optional — bypass gateway)
OPENAI_API_KEY=sk-...
+1 -1
View File
@@ -66,7 +66,7 @@ Upload files to conversations for context-aware responses using Retrieval-Augmen
```bash
# Embeddings come from Hanzo. The RAG API's key/base-URL variables are named
# for the wire format they speak; point them at api.hanzo.ai and use a Hanzo key.
RAG_OPENAI_API_KEY=hk-your-api-key
RAG_OPENAI_API_KEY=sk-your-api-key
RAG_OPENAI_BASEURL=https://api.hanzo.ai/v1
EMBEDDINGS_MODEL=zen-embedding
+1 -1
View File
@@ -52,7 +52,7 @@ services:
```bash
# Embeddings provider (required)
RAG_OPENAI_API_KEY=hk-your-api-key # a Hanzo key
RAG_OPENAI_API_KEY=sk-your-api-key # a Hanzo key
RAG_OPENAI_BASEURL=https://api.hanzo.ai/v1
# Embeddings model
+1 -1
View File
@@ -52,7 +52,7 @@ curl https://api.hanzo.ai/v1/chat/completions \
```python
from hanzoai import Hanzo
client = Hanzo(api_key="hk-your-api-key")
client = Hanzo(api_key="sk-your-api-key")
response = client.chat.completions.create(
model="zen5",
+1 -1
View File
@@ -27,7 +27,7 @@ The console is a single-page app embedded (`go:embed`) directly into the Hanzo C
```bash
# The console calls the same API you can call directly, with the same key
curl https://api.hanzo.ai/v1/models \
-H "Authorization: Bearer hk-..."
-H "Authorization: Bearer sk-..."
```
## Related
+1 -1
View File
@@ -13,7 +13,7 @@ Fetch one URL and get back clean markdown (scripts, nav, and boilerplate strippe
```bash
curl -X POST https://api.hanzo.ai/v1/scrape \
-H "Authorization: Bearer hk-..." \
-H "Authorization: Bearer sk-..." \
-H "Content-Type: application/json" \
-d '{
"url": "https://example.com",
+1 -1
View File
@@ -18,7 +18,7 @@ Every panel is scoped to your organization and reads real metrics. Usage is attr
## Reading Metrics via the API
The same numbers a panel renders are readable over the API with an API key (`hk-...`), so a dashboard is never the only place a metric exists. The endpoints and their parameters are in the [API reference](/docs/api).
The same numbers a panel renders are readable over the API with an API key (`sk-...`), so a dashboard is never the only place a metric exists. The endpoints and their parameters are in the [API reference](/docs/api).
## Related
+2 -2
View File
@@ -11,7 +11,7 @@ A dataset is a curated collection of **items** — each an input paired with an
```bash
curl -X POST https://api.hanzo.ai/v1/evals/datasets \
-H "Authorization: Bearer hk-..." \
-H "Authorization: Bearer sk-..." \
-H "Content-Type: application/json" \
-d '{
"name": "support-qa",
@@ -27,7 +27,7 @@ Items are versioned by dataset, so a run always evaluates against a known snapsh
```bash
curl https://api.hanzo.ai/v1/evals/datasets \
-H "Authorization: Bearer hk-..."
-H "Authorization: Bearer sk-..."
```
## Run against a model
+2 -2
View File
@@ -13,7 +13,7 @@ Each datastore is a logical resource scoped to your organization and provisioned
```bash
curl -X POST https://api.hanzo.ai/v1/datastore \
-H "Authorization: Bearer hk-..." \
-H "Authorization: Bearer sk-..." \
-H "X-Org-Id: org_a1b2c3" \
-H "Content-Type: application/json" \
-d '{ "name": "events" }'
@@ -41,5 +41,5 @@ Datastore is built for append-heavy analytics — events, logs, metrics, and tim
- [Analytics engine reference](/docs/services/analytics) — the full SQL surface and table engines
- [SQL](/docs/sql) — managed PostgreSQL for transactional data
- [Storage](/docs/storage) — S3-compatible object storage for cold data
- [API Keys](/docs/api-keys) — authenticate with `hk-` keys
- [API Keys](/docs/api-keys) — authenticate with `sk-` keys
- [API Reference](/docs/openapi) — every endpoint at `api.hanzo.ai`
+4 -4
View File
@@ -45,7 +45,7 @@ for k in ["/v1/projects","/v1/git","/v1/code","/v1/sites","/v1/templates","/v1/r
```bash
curl -X POST https://api.hanzo.ai/v1/projects/fork \
-H "Authorization: Bearer hk-..." \
-H "Authorization: Bearer sk-..." \
-H "X-Org-Id: org_a1b2c3" \
-H "Content-Type: application/json" \
-d '{ "template": "folio", "slug": "my-folio" }'
@@ -84,17 +84,17 @@ Publishing does not upload bytes. The builder's output already lives in Hanzo's
```bash
# create + activate in one call — the 99% path
curl -X POST https://api.hanzo.ai/v1/sites/my-folio/publish \
-H "Authorization: Bearer hk-..." -H "X-Org-Id: org_a1b2c3" \
-H "Authorization: Bearer sk-..." -H "X-Org-Id: org_a1b2c3" \
-H "Content-Type: application/json" \
-d '{ "source": "builds/my-folio/out" }'
# the rollback menu
curl https://api.hanzo.ai/v1/sites/my-folio/releases \
-H "Authorization: Bearer hk-..." -H "X-Org-Id: org_a1b2c3"
-H "Authorization: Bearer sk-..." -H "X-Org-Id: org_a1b2c3"
# rollback is the same flip, aimed at an older id. Nothing is rebuilt.
curl -X POST https://api.hanzo.ai/v1/sites/my-folio/releases/rel_<id>/activate \
-H "Authorization: Bearer hk-..." -H "X-Org-Id: org_a1b2c3"
-H "Authorization: Bearer sk-..." -H "X-Org-Id: org_a1b2c3"
```
`source` is a path **relative to your own org's storage**. The org segment comes from the validated principal and the bucket is server-owned and never appears in the request, so the worst a hostile source string can name is something your org already owns.
+2 -2
View File
@@ -13,7 +13,7 @@ Each database is a logical resource scoped to your organization and created thro
```bash
curl -X POST https://api.hanzo.ai/v1/docdb \
-H "Authorization: Bearer hk-..." \
-H "Authorization: Bearer sk-..." \
-H "X-Org-Id: org_a1b2c3" \
-H "Content-Type: application/json" \
-d '{ "name": "app" }'
@@ -58,5 +58,5 @@ DocDB suits flexible, schema-light document data. For relational data and SQL, u
- [SQL](/docs/sql) — the managed PostgreSQL that backs DocDB storage
- [Vector](/docs/vector) — embeddings and semantic search
- [Search](/docs/search) — full-text and hybrid indexes over documents
- [API Keys](/docs/api-keys) — authenticate with `hk-` keys
- [API Keys](/docs/api-keys) — authenticate with `sk-` keys
- [API Reference](/docs/openapi) — every endpoint at `api.hanzo.ai`
+4 -4
View File
@@ -6,7 +6,7 @@ description: Generate, store, and search vector embeddings at scale — the embe
# Hanzo Embeddings
Hanzo Embeddings turns text into vectors on the AI API — one endpoint, the same `hk-` key as everything else. Generate embeddings, then store and query them in [Vector](/docs/vector) for semantic search and RAG.
Hanzo Embeddings turns text into vectors on the AI API — one endpoint, the same `sk-` key as everything else. Generate embeddings, then store and query them in [Vector](/docs/vector) for semantic search and RAG.
## Generate Embeddings
@@ -14,7 +14,7 @@ Hanzo Embeddings turns text into vectors on the AI API — one endpoint, the sam
```bash
curl https://api.hanzo.ai/v1/embeddings \
-H "Authorization: Bearer hk-..." \
-H "Authorization: Bearer sk-..." \
-H "Content-Type: application/json" \
-d '{
"model": "zen-embedding",
@@ -42,7 +42,7 @@ Provision a [Vector](/docs/vector) collection whose `dimensions` match your mode
```python
from hanzoai import Hanzo
client = Hanzo(api_key="hk-...")
client = Hanzo(api_key="sk-...")
vecs = client.embeddings.create(
model="zen-embedding",
@@ -59,7 +59,7 @@ vecs = client.embeddings.create(
The catalogue also carries open-weight embedding models such as `bge-m3`. `GET /v1/models` lists every id this endpoint accepts — see [Models](/docs/models). Whichever you pick, set your Vector collection's `dimensions` from the length of the vector you actually get back rather than assuming it.
Pricing follows the AI API. Because embeddings share the gateway's per-key budgets and rate limits, the same `hk-` key governs spend across chat and embeddings.
Pricing follows the AI API. Because embeddings share the gateway's per-key budgets and rate limits, the same `sk-` key governs spend across chat and embeddings.
`/v1/embeddings` takes and returns the same JSON shapes as the widely-implemented embeddings format, so an HTTP client written against it works here once its base URL points at `https://api.hanzo.ai/v1` and it sends a Hanzo key.
+1 -1
View File
@@ -15,7 +15,7 @@ An experiment runs a [dataset](/docs/datasets) against a model — optionally wi
```bash
curl -X POST https://api.hanzo.ai/v1/evals/runs \
-H "Authorization: Bearer hk-..." \
-H "Authorization: Bearer sk-..." \
-H "Content-Type: application/json" \
-d '{
"dataset": "support-qa",
+2 -2
View File
@@ -87,7 +87,7 @@ Mint a key at [console.hanzo.ai](https://console.hanzo.ai) (see
```bash
curl https://api.hanzo.ai/v1/chat/completions \
-H "Authorization: Bearer hk-..." \
-H "Authorization: Bearer sk-..." \
-H "Content-Type: application/json" \
-d '{
"model": "enso",
@@ -103,7 +103,7 @@ Write the response to `snake.html` and open it. The same thing in Python:
```python
from hanzoai import Hanzo
client = Hanzo(api_key="hk-...")
client = Hanzo(api_key="sk-...")
game = client.chat.completions.create(
model="enso",
+2 -2
View File
@@ -15,7 +15,7 @@ The Hanzo Gateway is the single front door to the platform. Every request to `ap
"Gated" and "priced" are the gateway's job:
- **Gated** -- the gateway rejects unauthenticated or over-quota requests before they reach a backend. Present an API key (`hk-...`) or an IAM bearer token. Rate limits and circuit breakers protect every downstream service.
- **Gated** -- the gateway rejects unauthenticated or over-quota requests before they reach a backend. Present an API key (`sk-...`) or an IAM bearer token. Rate limits and circuit breakers protect every downstream service.
- **Priced** -- metered usage is attributed to your organization and priced by the commerce subsystem. See [Billing](/docs/billing) for credits, metering, and invoices.
The gateway forwards your `Authorization` header unchanged and streams responses (SSE) without transformation, so an HTTP client written against the chat-completions shape works against it once its base URL points here.
@@ -36,7 +36,7 @@ All services are reached under a single base -- `https://api.hanzo.ai/v1` -- nam
```bash
curl https://api.hanzo.ai/v1/chat/completions \
-H "Authorization: Bearer hk-..." \
-H "Authorization: Bearer sk-..." \
-H "Content-Type: application/json" \
-d '{
"model": "zen5",
+16 -13
View File
@@ -55,20 +55,22 @@ All resources -- API keys, models, usage logs -- are scoped to the active projec
Your key will look like this:
```
hk-proj-abc123def456...
sk-abc123def456...
```
The `hk-` prefix identifies it as a Hanzo API key. See [API Keys](/docs/api-keys) for a full breakdown of key types and prefixes.
The `sk-` prefix says this one is secret: it resolves to you, so it belongs on a
server. The other prefix, `pk-`, resolves only to your organization and is the
one you may put in a browser bundle. See [API Keys](/docs/api-keys).
## 5. Make Your First API Call
Every Hanzo service — AI, storage, identity, deploys — answers on **one host, `api.hanzo.ai`**, under `/v1/<service>`, with the same `hk-*` key. There is no second API host to configure.
Every Hanzo service — AI, storage, identity, deploys — answers on **one host, `api.hanzo.ai`**, under `/v1/<service>`, with the same `sk-*` key. There is no second API host to configure.
### curl
```bash
curl https://api.hanzo.ai/v1/chat/completions \
-H "Authorization: Bearer hk-your-api-key" \
-H "Authorization: Bearer sk-your-api-key" \
-H "Content-Type: application/json" \
-d '{
"model": "zen5",
@@ -80,7 +82,7 @@ List every model id the same way — `/v1/models` takes the same key:
```bash
curl https://api.hanzo.ai/v1/models \
-H "Authorization: Bearer hk-your-api-key"
-H "Authorization: Bearer sk-your-api-key"
```
### Python
@@ -94,7 +96,7 @@ pip install hanzoai
```python
from hanzoai import Hanzo
client = Hanzo(api_key="hk-your-api-key")
client = Hanzo(api_key="sk-your-api-key")
response = client.chat.completions.create(
model="zen5",
@@ -114,7 +116,7 @@ npm install @hanzo/ai
```typescript
import Hanzo from '@hanzo/ai'
const client = new Hanzo({ apiKey: 'hk-your-api-key' })
const client = new Hanzo({ apiKey: 'sk-your-api-key' })
const completion = await client.chat.completions.create({
model: 'zen5',
@@ -146,13 +148,14 @@ Building a funded startup? You may qualify for **up to $150,000 in credits** thr
## Key Types at a Glance
| Prefix | Type | Purpose |
|--------|------|---------|
| `hk-*` | API Key | Server-side API access. Scoped to a project. |
| `sk-*` | Secret Key | Provider-level keys (e.g. upstream model providers). Never expose client-side. |
| `hz-*` | Widget Key | Client-side keys for browser widgets. Restricted scopes, safe to embed. |
| Prefix | Type | Resolves to | Where it belongs |
|--------|------|-------------|------------------|
| `sk-*` | Secret | You | A server. It authenticates as you, so it can do anything you can. |
| `pk-*` | Publishable | Your org | Anywhere, browser bundles included. It identifies, it does not authenticate. |
See [API Keys](/docs/api-keys) for details on scopes, rotation, and production best practices.
Two types, and that is the whole list — a key spelled any other way is refused at
the door. See [API Keys](/docs/api-keys) for what each one can do, how to rotate
them, and how to keep them out of your source tree.
## What Next?
+2 -2
View File
@@ -42,12 +42,12 @@ See [Authentication](/docs/authentication) for the full OAuth 2.0 + PKCE flow, t
## Managing Identity via the API
Identity resources are served under `/v1/iam` on the gateway. Authenticate with an API key (`hk-...`) or an IAM bearer token; requests are scoped to the caller's organization.
Identity resources are served under `/v1/iam` on the gateway. Authenticate with an API key (`sk-...`) or an IAM bearer token; requests are scoped to the caller's organization.
```bash
# List users in your organization
curl https://api.hanzo.ai/v1/iam/users \
-H "Authorization: Bearer hk-..."
-H "Authorization: Bearer sk-..."
```
Every request shares the same shape -- `https://api.hanzo.ai/v1/<service>/...` with a bearer token. For the complete IAM resource reference (organizations, applications, roles, permissions, providers), see [Hanzo IAM](/docs/services/iam) and the [API reference](/docs/api).
+1 -1
View File
@@ -26,7 +26,7 @@ List and manage a project's connections over the API. The exact fields vary per
```bash
curl https://api.hanzo.ai/v1/integrations \
-H "Authorization: Bearer hk-..."
-H "Authorization: Bearer sk-..."
```
```json
+25 -11
View File
@@ -21,12 +21,12 @@ KMS is zero-knowledge by design: values are encrypted at rest, access is governe
## Secrets and Cryptographic Operations
Secrets and key operations are served under `/v1/kms` on the gateway, or directly at `kms.hanzo.ai`. Authenticate with an API key (`hk-...`).
Secrets and key operations are served under `/v1/kms` on the gateway, or directly at `kms.hanzo.ai`. Authenticate with an API key (`sk-...`).
```bash
# Read a secret from an environment path
curl "https://api.hanzo.ai/v1/kms/secrets?environment=production&path=/api-keys" \
-H "Authorization: Bearer hk-..."
-H "Authorization: Bearer sk-..."
```
Keys never leave the boundary. For signing and threshold operations, KMS delegates to MPC and returns only the signature -- the private key material is split across independent nodes and never reconstructed.
@@ -39,20 +39,34 @@ For workloads on Kubernetes, the KMS operator syncs secrets into the cluster wit
apiVersion: secrets.lux.network/v1alpha1
kind: KMSSecret
metadata:
name: hanzo-api-key
name: api-kms-sync
namespace: acme
spec:
secretStoreRef:
name: kms-hanzo
data:
- secretKey: HANZO_API_KEY
remoteRef:
secretPath: /prod/api-keys
secretKey: primary
hostAPI: https://kms.hanzo.ai
resyncInterval: 60
authentication:
universalAuth:
credentialsRef:
secretName: acme-iam-creds
secretNamespace: acme
secretsScope:
projectSlug: acme
envSlug: prod
secretsPath: /api-keys
keys:
- HANZO_API_KEY
managedSecretReference:
creationPolicy: Orphan
secretName: api-secrets
secretNamespace: acme
secretType: Opaque
```
Every field is in the [KMSSecret reference](/docs/services/kms/kmssecret-crd).
## Related
- [API Keys](/docs/api-keys) -- store and rotate `hk-` keys in KMS for production
- [API Keys](/docs/api-keys) -- store and rotate `sk-` keys in KMS for production
- [MPC](/docs/mpc) -- threshold signing backend for KMS-managed keys
- [IAM](/docs/iam) -- identity that authorizes KMS access
- [Zero Trust](/docs/zero-trust) -- private access to services that consume secrets
+2 -2
View File
@@ -13,7 +13,7 @@ An instance is a logical namespace in the shared Hanzo KV backend, scoped to you
```bash
curl -X POST https://api.hanzo.ai/v1/kv \
-H "Authorization: Bearer hk-..." \
-H "Authorization: Bearer sk-..." \
-H "X-Org-Id: org_a1b2c3" \
-H "Content-Type: application/json" \
-d '{ "name": "cache", "eviction": "allkeys-lru" }'
@@ -54,5 +54,5 @@ Rich data structures (hashes, sets, sorted sets, bitmaps, HyperLogLog, geospatia
- [Hanzo KV service reference](/docs/services/kv) — the full command set, ACLs, and clustering
- [PubSub](/docs/pubsub) — durable, at-least-once messaging with JetStream
- [SQL](/docs/sql) — managed PostgreSQL for persistent relational data
- [API Keys](/docs/api-keys) — authenticate with `hk-` keys
- [API Keys](/docs/api-keys) — authenticate with `sk-` keys
- [API Reference](/docs/openapi) — every endpoint at `api.hanzo.ai`
+1 -1
View File
@@ -17,7 +17,7 @@ import { Sparkles } from 'lucide-react'
Hanzo Cloud, and know which model id to put in it.**
The AI API is where Hanzo Cloud runs inference. It sits on the same host as
every other Hanzo service — `api.hanzo.ai` — behind the same `hk-*` key. An
every other Hanzo service — `api.hanzo.ai` — behind the same `sk-*` key. An
application that already calls Hanzo for storage or search does not pick up a
second base URL or a second credential to start calling models.
+2 -2
View File
@@ -13,7 +13,7 @@ Emit logs through the OTLP endpoint, or write structured JSON to stdout and let
```bash
export OTEL_EXPORTER_OTLP_ENDPOINT=https://api.hanzo.ai/v1
export OTEL_EXPORTER_OTLP_HEADERS="Authorization=Bearer hk-...,X-Org-Id=org_123"
export OTEL_EXPORTER_OTLP_HEADERS="Authorization=Bearer sk-...,X-Org-Id=org_123"
export OTEL_SERVICE_NAME=checkout-api
```
@@ -25,7 +25,7 @@ Filter by service, level, and time window. The list response is the standard `{
```bash
curl "https://api.hanzo.ai/v1/logs?service=checkout-api&level=error&limit=100" \
-H "Authorization: Bearer hk-..."
-H "Authorization: Bearer sk-..."
```
```json
+3 -3
View File
@@ -17,7 +17,7 @@ The storefront is backed by the pricing catalog, which you can query directly:
```bash
curl https://api.hanzo.ai/v1/pricing/models \
-H "Authorization: Bearer hk-..."
-H "Authorization: Bearer sk-..."
```
```json
@@ -39,11 +39,11 @@ Prices are the real per-million-token rates; a model with no published price ren
## Deploy a Model
Once you have picked a model, call it through the [AI API](/docs/llm) with the same `hk-` key:
Once you have picked a model, call it through the [AI API](/docs/llm) with the same `sk-` key:
```bash
curl https://api.hanzo.ai/v1/chat/completions \
-H "Authorization: Bearer hk-..." \
-H "Authorization: Bearer sk-..." \
-H "Content-Type: application/json" \
-d '{
"model": "zen5",
+1 -1
View File
@@ -23,5 +23,5 @@ Memory builds on the same embedding and vector primitives as the rest of the pla
- [Embeddings](/docs/embeddings) — the vectors behind semantic recall
- [Vector](/docs/vector) — the managed vector database for custom RAG
- [MCP](/docs/mcp) — expose memory to agents as a tool
- [API Keys](/docs/api-keys) — authenticate with `hk-` keys
- [API Keys](/docs/api-keys) — authenticate with `sk-` keys
- [API Reference](/docs/openapi) — every endpoint at `api.hanzo.ai`
+3 -3
View File
@@ -9,7 +9,7 @@ Metrics are the numeric time series behind your product: request rates, token th
## Emitting metrics
Push metrics through the OTLP endpoint on the gateway, or expose a Prometheus `/metrics` endpoint and let O11y scrape it. Either way the same `hk-*` key authenticates and `X-Org-Id` scopes the series.
Push metrics through the OTLP endpoint on the gateway, or expose a Prometheus `/metrics` endpoint and let O11y scrape it. Either way the same `sk-*` key authenticates and `X-Org-Id` scopes the series.
```python
from prometheus_client import Counter, Histogram
@@ -30,7 +30,7 @@ For OTLP, point your exporter at the gateway — metrics land at `POST /v1/metri
```bash
export OTEL_EXPORTER_OTLP_ENDPOINT=https://api.hanzo.ai/v1
export OTEL_EXPORTER_OTLP_HEADERS="Authorization=Bearer hk-...,X-Org-Id=org_123"
export OTEL_EXPORTER_OTLP_HEADERS="Authorization=Bearer sk-...,X-Org-Id=org_123"
```
## Querying with PromQL
@@ -39,7 +39,7 @@ Query stored series with PromQL over REST:
```bash
curl "https://api.hanzo.ai/v1/metrics/query" \
-H "Authorization: Bearer hk-..." \
-H "Authorization: Bearer sk-..." \
--data-urlencode 'query=rate(inference_requests_total[5m])'
```
+7 -4
View File
@@ -26,8 +26,8 @@ Every resource in Hanzo is scoped to exactly one organization:
| Resource | Scoped To |
|----------|-----------|
| **Projects** | Organization |
| **API Keys** (`hk-*`, `hz-*`) | Project (within an org) |
| **Secret Keys** (`sk-*`) | Organization |
| **Secret keys** (`sk-*`) | The member who minted it — one per user |
| **Publishable keys** (`pk-*`) | Organization, or the project it was minted with |
| **Usage & Billing** | Organization |
| **Team Members** | Organization |
| **KMS Secrets** | Organization + project |
@@ -89,9 +89,12 @@ For more on tasks, see [Hanzo Tasks](/docs/tasks).
When using OIDC tokens (from OAuth login), the organization is embedded in the JWT `owner` claim. The API gateway reads this claim and scopes all requests to that organization.
When using API keys (`hk-*`), the organization is determined by the project the key belongs to. No additional header is needed.
A secret key (`sk-*`) resolves to the member who minted it, so requests scope to
that member's organization. A publishable key (`pk-*`) resolves to the
organization itself — or to the project it was minted with. Either way no
additional header is needed.
For administrative operations that span an organization (e.g. listing all projects), use an org-level secret key (`sk-*`):
For administrative operations that span an organization (e.g. listing all projects), use a secret key (`sk-*`) held by a member with the rights to do it:
```bash
curl -H "Authorization: Bearer sk-org-abc123..." \
+3 -3
View File
@@ -14,7 +14,7 @@ Prompt management with immutable versions: every edit creates a new version, and
```bash
curl -X POST https://api.hanzo.ai/v1/prompts \
-H "Authorization: Bearer hk-..." \
-H "Authorization: Bearer sk-..." \
-H "Content-Type: application/json" \
-d '{
"name": "support-reply",
@@ -32,11 +32,11 @@ List prompt metadata, or fetch one by name to get its full version history. Beca
```bash
# List prompts (name, versions, labels, tags)
curl https://api.hanzo.ai/v1/prompts \
-H "Authorization: Bearer hk-..."
-H "Authorization: Bearer sk-..."
# One prompt, with version history
curl https://api.hanzo.ai/v1/prompts/support-reply \
-H "Authorization: Bearer hk-..."
-H "Authorization: Bearer sk-..."
```
```json
+2 -2
View File
@@ -12,7 +12,7 @@ Hanzo Registry is a unified, OCI-compliant registry for container images and mod
Log in with your Hanzo API key, then push and pull with Docker, Podman, Buildah, or any OCI client.
```bash
# Log in (username `hanzo`, password is your hk- key)
# Log in (username `hanzo`, password is your sk- key)
echo "$HANZO_API_KEY" | docker login registry.hanzo.ai -u hanzo --password-stdin
# Tag and push
@@ -40,5 +40,5 @@ Blobs are stored on [Storage](/docs/storage) (Hanzo S3), so the registry inherit
- [Hanzo Registry service reference](/docs/services/registry) — tagging, model registry, and CI/CD
- [Storage](/docs/storage) — the S3-compatible backend for registry blobs
- [API Keys](/docs/api-keys) — authenticate `docker login` with `hk-` keys
- [API Keys](/docs/api-keys) — authenticate `docker login` with `sk-` keys
- [API Reference](/docs/openapi) — every endpoint at `api.hanzo.ai`
+3 -3
View File
@@ -51,16 +51,16 @@ See [Organizations](/docs/organizations) for the full role matrix.
From the console, **Projects** → **New Project** → **API Keys** → **Create Key**.
```
hk-proj-org-startx-abc123...
sk-abc123...
```
Copy it now. Set it in your shell:
```bash
export HANZO_API_KEY="hk-proj-org-startx-abc123..."
export HANZO_API_KEY="sk-abc123..."
```
This single key is the credential for every Hanzo service. The gateway routes calls based on the key's project scope.
This single key is the credential for every Hanzo service. A secret key resolves to the member who minted it, so calls carry that member's identity and their organization.
## 4. Seed secrets into KMS
+2 -2
View File
@@ -26,7 +26,7 @@ import (
Set your key:
```bash
export HANZO_API_KEY="hk-..." # from console.hanzo.ai
export HANZO_API_KEY="sk-..." # from console.hanzo.ai
```
## Quick start
@@ -44,7 +44,7 @@ import (
func main() {
client := hanzoai.NewClient(
option.WithAPIKey("hk-your-key"), // or HANZO_API_KEY
option.WithAPIKey("sk-your-key"), // or HANZO_API_KEY
)
resp, err := client.Chat.Completions.New(context.TODO(), hanzoai.ChatCompletionNewParams{
+4 -4
View File
@@ -113,7 +113,7 @@ resource:
```python tab="Python"
from hanzoai import Hanzo
client = Hanzo(api_key="hk-your-key")
client = Hanzo(api_key="sk-your-key")
response = client.chat.completions.create(
model="zen5",
messages=[{"role": "user", "content": "Hello"}],
@@ -124,7 +124,7 @@ print(response.choices[0].message.content)
```typescript tab="TypeScript"
import Hanzo from '@hanzo/sdk';
const client = new Hanzo({ apiKey: 'hk-your-key' });
const client = new Hanzo({ apiKey: 'sk-your-key' });
const response = await client.chat.completions.create({
model: 'zen5',
messages: [{ role: 'user', content: 'Hello' }],
@@ -132,7 +132,7 @@ const response = await client.chat.completions.create({
```
```go tab="Go"
client := hanzoai.NewClient(option.WithAPIKey("hk-your-key"))
client := hanzoai.NewClient(option.WithAPIKey("sk-your-key"))
response, _ := client.Chat.Completions.New(ctx, hanzoai.ChatCompletionNewParams{
Model: hanzoai.F("zen5"),
Messages: hanzoai.F([]hanzoai.ChatCompletionMessageParamUnion{
@@ -144,7 +144,7 @@ response, _ := client.Chat.Completions.New(ctx, hanzoai.ChatCompletionNewParams{
## One key, one base URL
```bash
export HANZO_API_KEY="hk-..." # from console.hanzo.ai
export HANZO_API_KEY="sk-..." # from console.hanzo.ai
# Base URL for every service: https://api.hanzo.ai/v1
```
+4 -4
View File
@@ -35,7 +35,7 @@ pip install hanzoai
Both are generally available on PyPI. Set your key once:
```bash
export HANZO_API_KEY="hk-..." # from console.hanzo.ai
export HANZO_API_KEY="sk-..." # from console.hanzo.ai
```
## Quick start — Cloud SDK
@@ -46,7 +46,7 @@ for chat:
```python
from hanzoai import Hanzo
client = Hanzo(api_key="hk-your-key") # or reads HANZO_API_KEY
client = Hanzo(api_key="sk-your-key") # or reads HANZO_API_KEY
response = client.chat.completions.create(
model="zen5",
@@ -75,7 +75,7 @@ import asyncio
from hanzoai import AsyncHanzo
async def main():
client = AsyncHanzo(api_key="hk-your-key")
client = AsyncHanzo(api_key="sk-your-key")
response = await client.chat.completions.create(
model="zen5",
messages=[{"role": "user", "content": "Hello!"}],
@@ -131,7 +131,7 @@ response = client.chat.completions.create(
```python
client = Hanzo(
api_key="hk-your-key",
api_key="sk-your-key",
base_url="https://api.hanzo.ai/v1", # default
timeout=60.0,
max_retries=2,
+1 -1
View File
@@ -35,7 +35,7 @@ tokio = { version = "1", features = ["full"] }
Set your key:
```bash
export HANZO_API_KEY="hk-..." # from console.hanzo.ai
export HANZO_API_KEY="sk-..." # from console.hanzo.ai
```
## Quick start
+1 -1
View File
@@ -43,7 +43,7 @@ bun add @hanzo/sdk
Set your key once:
```bash
export HANZO_API_KEY="hk-..." # from console.hanzo.ai
export HANZO_API_KEY="sk-..." # from console.hanzo.ai
```
## Quick start — Cloud SDK
+2 -2
View File
@@ -13,7 +13,7 @@ An index is a logical resource scoped to your organization. It is created on fir
```bash
curl -X POST https://api.hanzo.ai/v1/search/indexes \
-H "Authorization: Bearer hk-..." \
-H "Authorization: Bearer sk-..." \
-H "X-Org-Id: org_a1b2c3" \
-H "Content-Type: application/json" \
-d '{ "uid": "products", "primaryKey": "id" }'
@@ -40,5 +40,5 @@ Configure an embedder on the index, then blend keyword and semantic ranking with
- [Hanzo Search service reference](/docs/services/search) — the full indexing, settings, and chat API
- [Vector](/docs/vector) — dense-only similarity search for RAG pipelines
- [Embeddings](/docs/embeddings) — generate the vectors that power hybrid search
- [API Keys](/docs/api-keys) — authenticate with `hk-` keys
- [API Keys](/docs/api-keys) — authenticate with `sk-` keys
- [API Reference](/docs/openapi) — every endpoint at `api.hanzo.ai`
+1 -1
View File
@@ -215,7 +215,7 @@ const agent = new Agent({
## IAM and KMS
Agents authenticate to the control plane with the same `hk-*` API key as the rest of the stack. The control plane:
Agents authenticate to the control plane with the same `sk-*` API key as the rest of the stack. The control plane:
- Calls [Hanzo IAM](/docs/services/iam) to validate the token and resolve the org.
- Reads any per-agent secrets from [Hanzo KMS](/docs/services/kms) (model API keys, tool credentials).
+1 -1
View File
@@ -239,7 +239,7 @@ agents:
```bash
# Required
HANZO_API_KEY=hk-your-api-key
HANZO_API_KEY=sk-your-api-key
# Database
MONGO_URI=mongodb://localhost:27017/chat
@@ -16,7 +16,7 @@ Hanzo Cloud is the core AI Cloud OS that powers `api.hanzo.ai`. It provides a un
- **Knowledge Base**: RAG-powered document stores with vector embeddings
- **Container Cloud**: Kubernetes application templates and management
- **Outside Providers**: Connect Anthropic, OpenAI, Fireworks or DO-AI when a workflow needs a specific model
- **IAM Authentication**: OAuth2/OIDC via Hanzo IAM, API keys (`hk-*`), widget keys (`hz-*`)
- **IAM Authentication**: OAuth2/OIDC via Hanzo IAM, secret keys (`sk-*`), publishable keys (`pk-*`)
- **Usage Billing**: Per-request metering via Hanzo Commerce API
- **Rate Limiting**: Dynamic tier-based limits (free → enterprise)
- **Node Management**: Remote infrastructure via VNC/RDP
@@ -66,7 +66,7 @@ Create an API key at [console.hanzo.ai](https://console.hanzo.ai).
```bash
curl https://api.hanzo.ai/v1/chat/completions \
-H "Authorization: Bearer hk-your-api-key" \
-H "Authorization: Bearer sk-your-api-key" \
-H "Content-Type: application/json" \
-d '{
"model": "zen5-flash",
@@ -79,7 +79,7 @@ curl https://api.hanzo.ai/v1/chat/completions \
```typescript
import Hanzo from '@hanzo/ai'
const client = new Hanzo({ apiKey: 'hk-your-api-key' })
const client = new Hanzo({ apiKey: 'sk-your-api-key' })
const response = await client.chat.completions.create({
model: 'zen5-flash',
@@ -101,20 +101,20 @@ Hanzo Cloud exposes three protocols for accessing AI services:
```bash
curl https://api.hanzo.ai/v1/chat/completions \
-H "Authorization: Bearer hk-your-key" \
-H "Authorization: Bearer sk-your-key" \
-d '{"model": "zen5-flash", "messages": [{"role": "user", "content": "Hello"}]}'
```
`/v1/chat/completions`, `/v1/embeddings` and `/v1/models` take and return the
chat-completions JSON shape, so an HTTP client already written against it works
here once its base URL points at `https://api.hanzo.ai/v1` and it sends an
`hk-*` key. Model ids do not carry across vendors — `zen5` and `enso` are Hanzo
`sk-*` key. Model ids do not carry across vendors — `zen5` and `enso` are Hanzo
models and resolve only here.
### MCP (Model Context Protocol)
```bash
npx @hanzo/mcp --api-key hk-your-key
npx @hanzo/mcp --api-key sk-your-key
```
Or configure in your AI client's MCP settings:
@@ -124,7 +124,7 @@ Or configure in your AI client's MCP settings:
"mcpServers": {
"hanzo": {
"command": "npx",
"args": ["@hanzo/mcp", "--api-key", "hk-your-key"]
"args": ["@hanzo/mcp", "--api-key", "sk-your-key"]
}
}
}
@@ -147,10 +147,9 @@ Hanzo Cloud supports multiple auth methods:
| Method | Format | Use Case |
|--------|--------|----------|
| IAM API Key | `hk-*` | Server-side API access |
| Secret key | `sk-*` | Server-side API access, as the user who minted it |
| Publishable key | `pk-*` | Browser bundles — analytics, product insights, error capture |
| IAM JWT | Bearer token | User sessions via hanzo.id |
| Provider Key | `sk-*` | Direct provider passthrough |
| Widget Key | `hz-*` | Public-facing chat widgets |
All auth flows integrate with [Hanzo IAM](/docs/services/iam) for identity management.
+4 -4
View File
@@ -51,7 +51,7 @@ Ingestion: Your App ──► SDK/HTTP ──► api.hanzo.ai/v1/console/* ─
### 1. Get an API Key
Sign in at `console.hanzo.ai` with Hanzo IAM SSO. Navigate to **Settings > API Keys** and create a new key. Keys are scoped to a project and prefixed with `hk-`.
Sign in at `console.hanzo.ai` with Hanzo IAM SSO. Navigate to **Settings > API Keys** and create a key. Take a secret key (`sk-`) for server-side tracing; take a publishable key (`pk-`) for the browser SDK, which resolves only to your org and so is safe to ship.
### 2. Send a Trace
@@ -90,7 +90,7 @@ Open `console.hanzo.ai`, select your project, and click **Traces**. Your trace a
```python
from hanzoai import Hanzo
client = Hanzo(api_key="hk-your-api-key", trace=True)
client = Hanzo(api_key="sk-your-api-key", trace=True)
# Every call is traced automatically
response = client.chat.completions.create(
@@ -139,7 +139,7 @@ response = client.chat.completions.create(
```typescript
import Hanzo from '@hanzo/ai'
const client = new Hanzo({ apiKey: 'hk-your-api-key', trace: true })
const client = new Hanzo({ apiKey: 'sk-your-api-key', trace: true })
const response = await client.chat.completions.create({
model: 'zen5',
@@ -153,7 +153,7 @@ const response = await client.chat.completions.create({
```typescript
import { ConsoleClient } from '@hanzo/ai/console'
const console = new ConsoleClient({ publicKey: 'hk-your-api-key' })
const console = new ConsoleClient({ publicKey: 'pk-your-key' })
const trace = console.trace({ name: 'rag-pipeline' })
const retrieval = trace.span({ name: 'retrieval', input: { query } })
@@ -432,21 +432,34 @@ kubectl create secret generic mpc-encryption-keys \
For production, store encryption keys in Hanzo KMS and sync via the KMS operator:
```yaml
apiVersion: secrets.hanzo.ai/v1
apiVersion: secrets.lux.network/v1alpha1
kind: KMSSecret
metadata:
name: mpc-encryption-keys
name: mpc-kms-sync
namespace: mpc
spec:
secretRef:
hostAPI: https://kms.hanzo.ai
resyncInterval: 60
authentication:
universalAuth:
credentialsRef:
secretName: mpc-iam-creds
secretNamespace: mpc
secretsScope:
projectSlug: hanzo
envSlug: prod
secretsPath: /mpc/encryption
keys:
- NODE_ENCRYPTION_KEY
managedSecretReference:
creationPolicy: Orphan
secretName: mpc-encryption-keys
data:
- secretKey: node-key
remoteRef:
secretPath: /mpc/encryption
secretKey: NODE_ENCRYPTION_KEY
secretNamespace: mpc
secretType: Opaque
```
Every field is in the [KMSSecret reference](/docs/services/kms/kmssecret-crd).
---
## Environment Variables
@@ -13,7 +13,7 @@ Hanzo Gateway is a **high-performance API gateway** that routes every Hanzo and
- **OpenAI-compatible**: `/v1/chat/completions`, `/v1/models`, etc. work with any OpenAI SDK
- **Host-based routing**: Transparent reverse proxy for 40+ Hanzo domains (hanzo.id, chat.hanzo.ai, platform.hanzo.ai, etc.)
- **JWT + billing middleware**: Validates IAM tokens via JWKS, checks Commerce billing balance, injects identity headers
- **Widget security**: Per-IP rate limiting and origin validation for public widget keys (`hz_*`)
- **Publishable-key security**: Per-IP rate limiting and origin validation for the browser key (`pk-*`)
- **ZAP binary transport**: TLS 1.3+PQ (post-quantum) listener for native binary protocol backends via `luxfi/zap`
- **Plugin system**: Server, client, and modifier plugins loaded at runtime
@@ -184,9 +184,9 @@ make logs-lux
Three gin middlewares run in order before any request reaches a backend:
1. **Auth middleware** (`auth_middleware.go`): Validates JWT via JWKS, checks billing via Commerce API (fail-open), injects `X-IAM-Org-Id`, `X-IAM-User-Id`, `X-IAM-User-Email` headers. API keys (`hk-*`, `sk-*`, `fw_*`, `hz_*`, `pk-*`) pass through without JWT validation.
1. **Auth middleware** (`auth_middleware.go`): Validates JWT via JWKS, checks billing via Commerce API (fail-open), injects `X-IAM-Org-Id`, `X-IAM-User-Id`, `X-IAM-User-Email` headers. An opaque API key — `sk-*` or `pk-*`, the two spellings the door recognizes — is resolved against IAM rather than parsed as a JWT.
2. **Widget security** (`widget_security.go`): Per-IP rate limiting (10 req/min default) and origin validation for widget keys (`hz_*`). Prevents abuse of public-facing widget credentials.
2. **Publishable-key security** (`widget_security.go`): Per-IP rate limiting (10 req/min default) and origin validation for `pk-*` keys. A publishable key ships in a browser bundle by design, so the origin is the only thing left to check.
3. **Host proxy** (`hostProxyMiddleware` in `legacy_transports.go`, reading the table `routes.go` compiles): Routes 40+ Hanzo domains to their backend K8s services via `httputil.ReverseProxy`. Supports WebSocket upgrades natively. Path-based routing for multi-backend hosts (e.g., `platform.hanzo.ai` routes `/v1/` to the platform API, `/` to paas-ui).
@@ -392,8 +392,8 @@ sd.go # Service discovery registration
| 429 Too Many Requests | Global or per-IP rate limit exceeded | Check `qos/ratelimit/router` in gateway.json; increase limits or add per-endpoint overrides |
| 401 Unauthorized (JWT) | Invalid or expired JWT token | Verify token against `https://hanzo.id/.well-known/jwks`; check `AUTH_ISSUER` matches |
| 402 Payment Required | User has zero or negative Commerce balance | Add funds; or disable billing with `BILLING_ENABLED=false` |
| 403 Forbidden (widget) | Widget key (`hz_*`) used from unauthorized origin | Add origin to `AllowedOrigins` in `widget_security.go`; or use `hk-*` API key instead |
| API key rejected | Key prefix not recognized | Recognized prefixes: `hk-`, `sk-`, `fw_`, `hz_`, `pk-` |
| 403 Forbidden (browser) | Publishable key (`pk-*`) used from an unlisted origin | Add the origin to `AllowedOrigins` in `widget_security.go`; server-side, use a secret key instead |
| API key rejected | Key spelled a way the door does not recognize | Two spellings exist: `sk-` and `pk-`. Anything else is refused, never defaulted. |
| Route not found (404) | Endpoint not in gateway.json or host not in hostRoutes | Add endpoint to config or host to `router_engine.go` |
| Config validation fails | JSON syntax error or schema violation | Run `make validate`; check against the v3 config schema |
| WebSocket not connecting | Host not in `hostRoutes` | Add host entry; `httputil.ReverseProxy` handles WS natively |
+23 -11
View File
@@ -164,23 +164,35 @@ secret = client.get_secret(
### K8s Secret Sync (KMSSecret CRD)
```yaml
apiVersion: kms.hanzo.ai/v1
apiVersion: secrets.lux.network/v1alpha1
kind: KMSSecret
metadata:
name: my-app-secrets
name: my-app-kms-sync
namespace: default
spec:
project: hanzo-paas
environment: production
syncInterval: 5m
secretRef:
name: my-app-secrets # K8s Secret to create/update
secrets:
- DATABASE_URL
- REDIS_URL
- HANZO_API_KEY
hostAPI: https://kms.hanzo.ai
resyncInterval: 60
authentication:
universalAuth:
credentialsRef:
secretName: my-app-iam-creds
secretNamespace: default
secretsScope:
projectSlug: hanzo-paas
envSlug: prod
secretsPath: /my-app
keys:
- DATABASE_URL
- HANZO_API_KEY
managedSecretReference:
creationPolicy: Orphan
secretName: my-app-secrets # K8s Secret to create/update
secretNamespace: default
secretType: Opaque
```
Every field is in the [KMSSecret reference](/docs/services/kms/kmssecret-crd).
The KMS operator (`hanzoai/kms-operator`) watches KMSSecret resources and auto-syncs to K8s Secrets.
### Helm Chart: Auto-Bootstrap
@@ -159,18 +159,34 @@ IAM_USERINFO_URL=https://hanzo.id/oauth/userinfo
Secrets are KMS-first — no plaintext in manifests:
```yaml
# KMSSecret CRD for auto-sync
apiVersion: kms.hanzo.ai/v1
# KMSSecret CRD for auto-sync — full field list at
# /docs/services/kms/kmssecret-crd
apiVersion: secrets.lux.network/v1alpha1
kind: KMSSecret
metadata:
name: hanzo-paas
name: paas-kms-sync
namespace: hanzo
spec:
project: hanzo-paas
environment: production
secrets:
- DOCKERHUB_USERNAME
- DOCKERHUB_TOKEN
- DIGITALOCEAN_ACCESS_TOKEN
hostAPI: https://kms.hanzo.ai
resyncInterval: 60
authentication:
universalAuth:
credentialsRef:
secretName: hanzo-paas-iam-creds
secretNamespace: hanzo
secretsScope:
projectSlug: hanzo-paas
envSlug: prod
secretsPath: /paas
keys:
- DOCKERHUB_USERNAME
- DOCKERHUB_TOKEN
- DIGITALOCEAN_ACCESS_TOKEN
managedSecretReference:
creationPolicy: Orphan
secretName: paas-secrets
secretNamespace: hanzo
secretType: Opaque
```
## White-Label / Self-Host
@@ -66,7 +66,7 @@ terraform {
}
provider "terraform-provider-hanzo" {
api_key = "hk-..." # or set HANZO_API_KEY env var
api_key = "sk-..." # or set HANZO_API_KEY env var
}
```
@@ -146,7 +146,7 @@ Platforms: linux/amd64, linux/arm64, darwin/amd64, darwin/arm64, windows/amd64,
## Environment Variables
```bash
HANZO_API_KEY=hk-... # API authentication (required)
HANZO_API_KEY=sk-... # API authentication (required)
HANZO_BASE_URL=https://... # Override API base URL
```
@@ -76,7 +76,7 @@ use hanzo_tunnel::{connect, TunnelConfig, AppKind};
async fn main() -> Result<(), hanzo_tunnel::TunnelError> {
let conn = connect(TunnelConfig {
relay_url: "wss://api.hanzo.ai/v1/relay".into(),
auth_token: "hk-abc123".into(),
auth_token: "sk-abc123".into(),
app_kind: AppKind::Dev,
display_name: "z-macbook".into(),
capabilities: vec!["chat".into(), "exec".into()],
+2 -2
View File
@@ -17,7 +17,7 @@ Databases are scoped to your organization. Creating one returns a ready-to-use c
```bash
curl -X POST https://api.hanzo.ai/v1/sql \
-H "Authorization: Bearer hk-..." \
-H "Authorization: Bearer sk-..." \
-H "X-Org-Id: org_a1b2c3" \
-H "Content-Type: application/json" \
-d '{ "name": "app", "region": "us-east" }'
@@ -59,7 +59,7 @@ Branches are copy-on-write clones that share storage with the parent until data
- [Hanzo DB engine reference](/docs/services/sql) — pooling modes, PITR, and pgvector indexing
- [Vector](/docs/vector) — a dedicated vector database for embedding-first workloads
- [DocDB](/docs/docdb) — MongoDB-compatible document storage
- [API Keys](/docs/api-keys) — authenticate with `hk-` keys
- [API Keys](/docs/api-keys) — authenticate with `sk-` keys
- [API Reference](/docs/openapi) — every endpoint at `api.hanzo.ai`
{/* domain-index */}
+2 -2
View File
@@ -13,7 +13,7 @@ Buckets are scoped to your organization and provisioned through the control plan
```bash
curl -X POST https://api.hanzo.ai/v1/s3 \
-H "Authorization: Bearer hk-..." \
-H "Authorization: Bearer sk-..." \
-H "X-Org-Id: org_a1b2c3" \
-H "Content-Type: application/json" \
-d '{ "name": "my-models", "versioning": true }'
@@ -59,5 +59,5 @@ Hanzo Storage is the durable backing layer for model weights, training datasets,
- [Hanzo S3 service reference](/docs/services/s3) — CLI, SDKs, policies, and lifecycle rules
- [Registry](/docs/registry) — container and model artifacts stored on Hanzo S3
- [Datastore](/docs/datastore) — analytics that read cold data directly from buckets
- [API Keys](/docs/api-keys) — authenticate with `hk-` keys
- [API Keys](/docs/api-keys) — authenticate with `sk-` keys
- [API Reference](/docs/openapi) — every endpoint at `api.hanzo.ai`
+5 -5
View File
@@ -9,11 +9,11 @@ A trace is one end-to-end request or agent run, broken into nested **observation
## Ingesting traces
Point any OpenTelemetry SDK or collector at the gateway. Tenancy travels in the export headers: the `hk-*` key authenticates and `X-Org-Id` scopes the data to your org.
Point any OpenTelemetry SDK or collector at the gateway. Tenancy travels in the export headers: the `sk-*` key authenticates and `X-Org-Id` scopes the data to your org.
```bash
export OTEL_EXPORTER_OTLP_ENDPOINT=https://api.hanzo.ai/v1
export OTEL_EXPORTER_OTLP_HEADERS="Authorization=Bearer hk-...,X-Org-Id=org_123"
export OTEL_EXPORTER_OTLP_HEADERS="Authorization=Bearer sk-...,X-Org-Id=org_123"
export OTEL_SERVICE_NAME=checkout-api
export OTEL_RESOURCE_ATTRIBUTES="deployment.environment=production"
```
@@ -27,11 +27,11 @@ Traces are queried over REST. List endpoints return `{ data, meta }` with `page`
```bash
# List recent traces
curl "https://api.hanzo.ai/v1/o11y/traces?page=1&limit=50" \
-H "Authorization: Bearer hk-..."
-H "Authorization: Bearer sk-..."
# Fetch one trace with its observations + scores
curl https://api.hanzo.ai/v1/o11y/traces/tr_abc123 \
-H "Authorization: Bearer hk-..."
-H "Authorization: Bearer sk-..."
```
```json
@@ -66,4 +66,4 @@ Each trace carries request-level metadata and a list of observations. An observa
- [Sessions](/docs/sessions) — traces grouped into multi-turn runs
- [Scores](/docs/scores) — evaluations attached to traces and observations
- [Logs](/docs/logs) and [Metrics](/docs/metrics) — the other two observability signals
- [API Keys](/docs/api-keys) — creating the `hk-*` key these calls use
- [API Keys](/docs/api-keys) — creating the `sk-*` key these calls use
+2 -2
View File
@@ -13,7 +13,7 @@ A collection is a logical resource in the shared Hanzo Vector backend, scoped to
```bash
curl -X POST https://api.hanzo.ai/v1/vector \
-H "Authorization: Bearer hk-..." \
-H "Authorization: Bearer sk-..." \
-H "X-Org-Id: org_a1b2c3" \
-H "Content-Type: application/json" \
-d '{
@@ -46,5 +46,5 @@ The backend is API-compatible with Qdrant, so any Qdrant client works against yo
- [Hanzo Vector service reference](/docs/services/vector) — full collection, points, and snapshot API
- [Embeddings](/docs/embeddings) — generate the vectors you store here
- [Search](/docs/search) — full-text and hybrid indexes
- [API Keys](/docs/api-keys) — authenticate with `hk-` keys
- [API Keys](/docs/api-keys) — authenticate with `sk-` keys
- [API Reference](/docs/openapi) — every endpoint at `api.hanzo.ai`
+198
View File
@@ -0,0 +1,198 @@
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { DOCUMENT, loadDocument, type KeyType } from './openapi-doc';
// No page may spell a Hanzo API key a way cloud does not mint.
//
// This is check-endpoints' rule applied to the other half of a first call. That
// guard stops a page naming a route the API does not serve; this one stops a
// page naming a CREDENTIAL the API does not issue, which fails a reader one step
// earlier — they cannot even ask for the key the page tells them to get.
//
// It was written because docs.hanzo.ai documented three key types and cloud has
// two. `hk-` and `hz-` appeared on 148 pages, including the authentication page,
// and neither has ever existed: cloud's door takes `pk-` and `sk-` and refuses
// everything else. Prose cannot be trusted to remember that, so it is checked.
//
// BOTH directions, which is what makes the pages underivable-from-memory:
//
// unknown a page spells a prefix the document does not carry -> fail
// missing the authentication page omits a class the document DOES carry -> fail
//
// So renaming a class in cloud fails the build, adding one fails the build until
// it is documented, and removing one fails the build while a page still teaches
// it. A prefix on these pages is therefore a projection of cloud with a checked
// arrow back to it, not a value someone typed and nobody re-read.
const SCRIPT_DIR = path.dirname(fileURLToPath(import.meta.url));
const APP_ROOT = path.resolve(SCRIPT_DIR, '..');
const CONTENT = path.join(APP_ROOT, 'content/docs');
/** Where the key classes are taught. The `missing` half of the rule applies here. */
const AUTHORITY = 'api-keys.mdx';
// Ported upstream docs describe other people's credentials — Stripe's `sk_`,
// GitHub's `ghp_` — mirrored, not authored here. Same exemption check-endpoints
// makes, for the same reason: they are not ours to hold to our document.
const SKIP = ['projects/'];
/**
* What a credential looks like, and where one appears. Both halves are needed,
* and the first half is read off the document rather than chosen.
*
* SHAPE. A prefix is some lowercase letters and a hyphen, and how many letters
* is not this file's opinion — it is measured from the prefixes the document
* carries. `sk-` and `pk-` are two letters, so two letters is what a Hanzo key
* prefix is today, and the day cloud mints a longer one the guard widens with
* it. That one derived number is what separates `hk-your-api-key` from a bare
* `your-api-key`: the first wears a prefix, the second IS the placeholder.
*
* POSITION. Shape alone separates nothing — `us-east-1` and `do-sfo3` are two
* letters and a hyphen too. A credential appears in exactly three places, and
* none of them holds a region or a model:
*
* PRESENTED Bearer hk-… the header a reader will send
* ASSIGNED api_key="hk-…", HANZO_API_KEY=… an identifier ending in key or
* token, given a prefixed value
* NAMED `hk-` `hk-*` `hk-...` the prefix as its own code span
*
* This is check-endpoints' anchor argument applied to credentials: that guard
* only reads a path written against `api.hanzo.ai`, because a bare `/v1/acs` is
* on somebody else's host. A bare hyphenated word is somebody else's noun.
*
* NAMED deliberately does NOT admit a body that says the word key. It looks like
* it should — `hk-your-api-key` is plainly a credential — but every literal of
* that form on this site is already in a Bearer or an assignment, while the
* shape by itself sweeps up `iam-secrets`, `oidc-client-secret`, `data-api-key`
* and every other Kubernetes Secret named on these pages. It bought nothing and
* cost fourteen wrong findings.
*
* One thing this cannot tell apart, and does not pretend to: an upstream
* provider's key that happens to share a spelling. `OPENAI_API_KEY=sk-…` reads
* as `sk-`, which is also Hanzo's secret prefix, so it passes. It would only
* ever pass — a shared spelling cannot produce a wrong refusal — so the
* ambiguity costs a reader nothing.
*/
const PRESENTED = (n: string) => new RegExp(String.raw`\bBearer\s+["'\`]?([a-z]{${n}}-)`, 'gi');
const ASSIGNED = (n: string) =>
new RegExp(
String.raw`[A-Za-z_][A-Za-z0-9_-]*(?:key|token)["'\`]?\s*(?:[:=]\s*["'\`]?|\(\s*["'\`])([a-z]{${n}}-)`,
'gi',
);
const NAMED = (n: string) =>
new RegExp(String.raw`(?:\`|")([a-z]{${n}}-)(\*|\.\.\.)?(?:\`|")`, 'g');
/** The prefix lengths the document uses, as a regex quantifier: `2`, or `2,4`. */
export function prefixShape(keys: KeyType[]): string {
const lens = [...new Set(keys.map((k) => k.prefix.replace('-', '').length))].sort((a, b) => a - b);
return lens.length === 1 ? String(lens[0]) : `${lens[0]},${lens.at(-1)}`;
}
function walk(dir: string, out: string[] = []): string[] {
for (const e of fs.readdirSync(dir, { withFileTypes: true })) {
const p = path.join(dir, e.name);
if (e.isDirectory()) walk(p, out);
else if (e.isFile() && (e.name.endsWith('.mdx') || e.name.endsWith('.md'))) out.push(p);
}
return out;
}
/**
* A page as the build compiles it, with `<include>` spliced in.
*
* The key-type table is generated from the document and included, so reading the
* raw source would find no prefixes on the very page whose job is to teach them
* — and the `missing` half of the rule would fail on the page that is most
* right. remarkInclude resolves relative to the including file; so does this.
*
* A target that is not there THROWS, and that is not defensiveness. The first
* version returned the unresolved tag instead, and an include written one
* directory short passed every check while the page it was on rendered nothing
* at all — the body silently empty, frontmatter and chrome intact, so the page
* still had its title in the sidebar and its description in the search index.
* A guard that tolerates a broken include is checking a page nobody can read.
*/
const INCLUDE = /<include>\s*([^<\s#]+)[^<]*<\/include>/g;
function compiled(file: string, depth = 0): string {
const src = fs.readFileSync(file, 'utf8');
if (depth > 3) return src;
return src.replace(INCLUDE, (_whole, rel: string) => {
const target = path.resolve(path.dirname(file), rel);
if (!fs.existsSync(target)) {
throw new Error(`${file} includes ${rel}, which does not exist — the page renders empty`);
}
return compiled(target, depth + 1);
});
}
/** Every credential prefix a page spells, deduplicated and lowercased. */
export function prefixesIn(src: string, shape: string): Set<string> {
const found = new Set<string>();
for (const re of [PRESENTED(shape), ASSIGNED(shape), NAMED(shape)]) {
for (const m of src.matchAll(re)) found.add(m[1].toLowerCase());
}
return found;
}
export interface KeyCheck {
/** How many credential literals were read across the content tree. */
checked: number;
/** The classes the document carries — what the pages are checked against. */
keys: KeyType[];
/** Page, and the prefix it spells that cloud does not mint. */
unknown: Array<[string, string]>;
/** Classes the document carries that the authority page never teaches. */
missing: string[];
}
export function checkKeys(): KeyCheck {
const doc = loadDocument(DOCUMENT);
const known = new Set(doc.keys.map((k) => k.prefix));
const shape = prefixShape(doc.keys);
const unknown: Array<[string, string]> = [];
let checked = 0;
let taught = new Set<string>();
for (const file of walk(CONTENT)) {
const rel = path.relative(CONTENT, file);
if (SKIP.some((s) => rel.startsWith(s))) continue;
const found = prefixesIn(compiled(file), shape);
checked += found.size;
if (rel === AUTHORITY) taught = found;
for (const p of found) if (!known.has(p)) unknown.push([rel, p]);
}
return {
checked,
keys: doc.keys,
unknown,
missing: doc.keys.filter((k) => !taught.has(k.prefix)).map((k) => `${k.name} (${k.prefix})`),
};
}
/** Report every page that spells a prefix cloud does not mint. */
export function report(unknown: Array<[string, string]>): void {
const byFile = new Map<string, string[]>();
for (const [f, p] of unknown) byFile.set(f, [...(byFile.get(f) ?? []), p]);
for (const [f, ps] of byFile) console.error(` ${f}: ${[...new Set(ps)].join(', ')}`);
}
if (import.meta.main) {
const { checked, keys, unknown, missing } = checkKeys();
console.log(
`[keys] the document mints ${keys.map((k) => `${k.name} ${k.prefix}`).join(', ')}; ` +
`${checked} credential literals read`,
);
if (unknown.length) {
console.error(`[keys] FAIL — ${unknown.length} spell a prefix cloud does not mint:`);
report(unknown);
}
if (missing.length) {
console.error(`[keys] FAIL — ${AUTHORITY} never teaches: ${missing.join(', ')}`);
}
if (unknown.length || missing.length) process.exit(1);
console.log('[keys] every prefix on every page is one the document carries');
}
+2 -2
View File
@@ -2,7 +2,7 @@ import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { parse as parseYaml } from 'yaml';
import { DOCUMENT, loadDocument, type Document, type Operation } from './openapi-doc';
import { DOCUMENT, loadDocument, secretKey, type Document, type Operation } from './openapi-doc';
import { SDKS, cli, http, mcp } from './openapi-surfaces';
import { MCP_DOOR, loadMcpTools } from './sync-mcp-tools';
import type { CliCommand } from './sync-cli-commands';
@@ -253,7 +253,7 @@ function renderIndex(flows: Flow[], doc: Document): string {
L.push('## One credential');
L.push('');
L.push('```bash');
L.push('export HANZO_API_KEY=hk-... # console.hanzo.ai');
L.push(`export HANZO_API_KEY=${secretKey(doc).prefix}... # console.hanzo.ai`);
L.push('hanzo auth login # or sign in through Hanzo IAM');
L.push('```');
L.push('');
+74
View File
@@ -0,0 +1,74 @@
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { DOCUMENT, loadDocument, type KeyType } from './openapi-doc';
import { text } from './mdx';
// THE KEY TYPES, generated from THE document.
//
// /docs/api-keys is the first page a reader needs and the one that was most
// wrong: it documented three key types, cloud mints two, and neither of the
// other two has ever existed. It went wrong the ordinary way — someone typed
// what a key looked like, and nothing afterwards ever compared the two.
//
// So the table is not typed. `/v1/keys` is the key resource, its Go source names
// each class beside its prefix, zipdoc lifts those comments into the document,
// and openapi-doc reads them back as Document.keys. This renders that. The prose
// around it on the page stays authored, because how to keep a credential out of
// a git history is not something the API describes — but every prefix, every
// class name and every "what it resolves to" comes from the handler.
//
// It is included rather than routed: `<include>` splices this into api-keys.mdx
// at compile time, so the fact appears in the page and this file is not a page.
// It lives outside content/ because source.config globs `**/*.mdx` there and
// would give a fragment its own URL.
const SCRIPT_DIR = path.dirname(fileURLToPath(import.meta.url));
const APP_ROOT = path.resolve(SCRIPT_DIR, '..');
export const KEY_TYPES_FRAGMENT = path.join(APP_ROOT, 'generated/key-types.mdx');
/**
* The one-line reading of a class, for the table's last column.
*
* The document states each class as `"secret" (sk-, session-equivalent, belongs
* on a server)`, and that parenthetical is the sentence worth publishing — it is
* the handler's own summary of what the class is FOR. Where a class carries no
* note, the class name stands alone rather than inventing one.
*/
const reading = (k: KeyType): string => (k.note ? text(k.note) : `The ${k.name} key.`);
export function genKeyTypes(): KeyType[] {
const doc = loadDocument(DOCUMENT);
const rows = doc.keys
.map((k) => `| \`${k.prefix}\` | ${text(k.name)} | ${reading(k)} |`)
.join('\n');
const browser = doc.keys.find((k) => k.publishable)!;
const server = doc.keys.filter((k) => !k.publishable);
const body = `{/* Generated by scripts/gen-key-types.ts from hanzoai/cloud's openapi.yaml.
Every value below is the /v1/keys handler's own doc comment. Do not edit. */}
| Prefix | Type | What it is |
|--------|------|------------|
${rows}
${
server.length === 1
? `\`${server[0].prefix}\` belongs on a server and \`${browser.prefix}\` may ship in a browser bundle.`
: `\`${browser.prefix}\` may ship in a browser bundle; every other type belongs on a server.`
} A key spelled any other way is refused at the door — never guessed at, never
defaulted to the safer-sounding one.
`;
fs.mkdirSync(path.dirname(KEY_TYPES_FRAGMENT), { recursive: true });
fs.writeFileSync(KEY_TYPES_FRAGMENT, body);
console.log(
`[keys] ${doc.keys.map((k) => `${k.name} ${k.prefix}`).join(', ')} -> ` +
path.relative(APP_ROOT, KEY_TYPES_FRAGMENT),
);
return doc.keys;
}
if (import.meta.main) genKeyTypes();
+10 -2
View File
@@ -1,7 +1,15 @@
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { DOCUMENT, isInternal, loadDocument, release, type Document, type Operation } from './openapi-doc';
import {
DOCUMENT,
isInternal,
loadDocument,
release,
secretKey,
type Document,
type Operation,
} from './openapi-doc';
import { toolOperations } from './openapi-surfaces';
import { load, MCP_DOOR, type McpCatalog, type McpTool } from './sync-mcp-tools';
import { code, fence, firstSentence, prose, text, yamlString } from './mdx';
@@ -788,7 +796,7 @@ function renderIndex(cat: McpCatalog, doc: Document, groups: Map<string, McpTool
L.push(
...fence(
'bash',
`export HANZO_API_KEY=hk-...\ncurl -X POST ${cat.door} \\\n -H "Authorization: Bearer $HANZO_API_KEY" \\\n -H "Content-Type: application/json" \\\n -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'`,
`export HANZO_API_KEY=${secretKey(doc).prefix}...\ncurl -X POST ${cat.door} \\\n -H "Authorization: Bearer $HANZO_API_KEY" \\\n -H "Content-Type: application/json" \\\n -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'`,
),
);
L.push('');
+4 -1
View File
@@ -7,6 +7,7 @@ import {
DOCUMENT,
loadDocument,
publicDocument,
secretKey,
type Document,
type Operation,
type Product,
@@ -263,7 +264,9 @@ function renderIndex(products: Product[], doc: Document): string {
L.push('');
L.push('## Authentication');
L.push('');
L.push('Every product accepts the same bearer credential — a Hanzo IAM JWT or an `hk-` API key.');
L.push(
`Every product accepts the same bearer credential — a Hanzo IAM JWT or a \`${secretKey(doc).prefix}\` secret key.`,
);
L.push('');
L.push('```bash');
L.push(`curl -H "Authorization: Bearer $HANZO_API_KEY" ${doc.server}/v1/models`);
+112
View File
@@ -92,12 +92,34 @@ export interface Product {
operations: Operation[];
}
/**
* One class of API key, as the document states it at `/v1/keys`.
*
* The class name is the product's own noun — it is the literal a caller sends
* as `{"type": …}` — and the prefix is what a key of that class is spelled
* with. `publishable` is not a judgement made here; it is the class the product
* NAMES publishable, which is the same word cloud's `PublishablePrefix` is
* named after.
*/
export interface KeyType {
/** The class as a caller names it: `secret`, `publishable`. */
name: string;
/** The spelling a key of this class carries: `sk-`, `pk-`. */
prefix: string;
/** What the document says the class is, verbatim from inside the parens. */
note: string;
/** True for the class the product calls publishable — the browser-safe one. */
publishable: boolean;
}
export interface Document {
title: string;
version: string;
description: string;
server: string;
securitySchemes: Record<string, any>;
/** Every class of API key the document says `/v1/keys` mints — see keyTypes. */
keys: KeyType[];
/** Products docs.hanzo.ai publishes. Excludes `internal` — see isInternal. */
products: Product[];
/** The operator surface, grouped the same way. Never published publicly. */
@@ -143,6 +165,17 @@ const INTERNAL_IDS = new Set(['get_v1_commerce_admin_catalog']);
export const isInternal = (op: Operation): boolean =>
op.product === INTERNAL_PRODUCT || INTERNAL_IDS.has(op.id);
/**
* The key a server presents: the one class that is NOT publishable.
*
* Every generated page that shows a curl or an env var shows this one, and each
* used to spell it as a literal — three copies of a value the document states,
* which is how three generated pages came to teach a prefix cloud has never
* minted. Asking the document is one expression and cannot drift.
*/
export const secretKey = (doc: Document): KeyType =>
doc.keys.find((k) => !k.publishable) ?? doc.keys[0];
/**
* The document with the operator surface taken out, for publishing.
*
@@ -183,6 +216,84 @@ export function deref(raw: any, node: any, depth = 0): any {
return deref(raw, cur, depth + 1);
}
/**
* The classes of API key, read out of the document rather than restated.
*
* `/v1/keys` IS the key resource: one endpoint, and the class is a field on it.
* Its Go source spells each class beside its prefix — `"secret" (sk-, …)`,
* `"publishable" (pk-, …)` — zipdoc lifts those doc comments into this
* document, and this reads them back. So a page saying `sk-` is saying what the
* handler says, one hop away, instead of what someone remembered.
*
* The pairs are read from EVERY description the key resource carries and the
* readings must AGREE. Two doc comments state them today — the `type` field a
* caller sends and the `type` field they read back — written in different words
* at different places in cloud. Requiring agreement means a rename that lands in
* one of them and not the other stops the build here rather than publishing two
* answers.
*
* Refusing is the whole point of the exercise: an empty or contradictory reading
* throws, because a docs build that quietly published no key types would be the
* same failure — a reader who cannot learn which credential to ask for — wearing
* a green checkmark.
*/
const KEY_CLASS = /"?\b([a-z][a-z-]{2,20})"?\s*\((([a-z]{2,6})-)([,)][^)]*)?\)/g;
export function keyTypes(raw: any): KeyType[] {
const item = raw?.paths?.['/v1/keys'];
if (!item) {
// Every generated page prints a bearer credential, so none of them can be
// written without knowing how one is spelled. Stopping here is the honest
// failure; the alternative is a whole site of curls with a blank key.
throw new Error('the document does not serve /v1/keys — no page can state how a key is spelled');
}
// Every sentence the key resource carries: its operations, their parameters,
// and the schemas they send and return. Naming one schema would tie this to a
// Go type name; walking what the path item reaches ties it to the resource.
const said: string[] = [];
const collect = (node: any, depth = 0) => {
if (!node || typeof node !== 'object' || depth > 6) return;
const d = deref(raw, node, 0);
if (typeof d?.description === 'string') said.push(d.description);
for (const v of Object.values(d)) if (v && typeof v === 'object') collect(v, depth + 1);
};
collect(item);
// class -> prefix, and every reading of a class must give the same prefix.
const seen = new Map<string, KeyType>();
for (const text of said) {
for (const m of text.replace(/\s+/g, ' ').matchAll(KEY_CLASS)) {
const name = m[1];
const prefix = `${m[3]}-`;
const note = (m[4] ?? '').replace(/^[,)]\s*/, '').trim();
const prior = seen.get(name);
if (prior && prior.prefix !== prefix) {
throw new Error(
`the document spells the ${name} key both ${prior.prefix} and ${prefix} — cloud says it two ways`,
);
}
// Keep the longest note: the same pair is stated tersely in one place and
// with its reason in another, and the reason is the half worth publishing.
if (!prior || note.length > prior.note.length) {
seen.set(name, { name, prefix, note, publishable: name === 'publishable' });
}
}
}
const found = [...seen.values()];
if (found.length < 2) {
throw new Error(
`/v1/keys names ${found.length} key class(es) — the document stopped stating them, so no page can`,
);
}
if (!found.some((k) => k.publishable)) {
throw new Error('no key class is named publishable — nothing may be shipped in a browser');
}
// Publishable last: a reader meets the default before the exception.
return found.sort((a, b) => Number(a.publishable) - Number(b.publishable));
}
/** `agents` -> `Agents`; `Roles & Permissions` and `MFA` pass through. */
const titleCase = (name: string): string =>
/^[a-z]/.test(name) ? name[0].toUpperCase() + name.slice(1) : name;
@@ -365,6 +476,7 @@ export function loadDocument(file: string): Document {
description: firstSentence(info.description ?? ''),
server: raw.servers?.[0]?.url ?? 'https://api.hanzo.ai',
securitySchemes: raw.components?.securitySchemes ?? {},
keys: keyTypes(raw),
// Only products the document actually serves operations for. A tag with no
// operations is not a product page — it is a tag we have not filled in yet.
//
+22
View File
@@ -3,11 +3,13 @@ import { genOpenapiPages } from './gen-openapi-pages';
import { genFlowPages } from './gen-flow-pages';
import { genMcpPages } from './gen-mcp-pages';
import { genPricingPage } from './gen-pricing-page';
import { genKeyTypes } from './gen-key-types';
import { syncCliCommands } from './sync-cli-commands';
import { syncMcpTools } from './sync-mcp-tools';
import { syncProjectDocs } from './sync-project-docs';
import { sanitizeMdx } from './sanitize-mdx';
import { checkEndpoints, report } from './check-endpoints';
import { checkKeys, report as reportKeys } from './check-keys';
async function main() {
// The document first, then its projections: the reference (one page per
@@ -20,6 +22,9 @@ async function main() {
await genOpenapiPages();
await genFlowPages();
await genMcpPages();
// The key types the authentication page teaches. Also from the document, and
// BEFORE the checks below, because the check reads what it writes.
genKeyTypes();
// The pricing page is the billing API's own answer, not the document's — it
// reads /v1/pricing, so it neither needs nor blocks the three above.
await genPricingPage();
@@ -43,6 +48,23 @@ async function main() {
throw new Error(`${unknown.length} mentions name endpoints the document does not have`);
}
console.log(`[endpoints] ${checked} mentions checked, every one in the document`);
// The same rule for the credential, which a reader needs one step before the
// endpoint: no page may spell a key a way cloud does not mint, and the page
// that teaches the key types must teach all of them.
const keys = checkKeys();
if (keys.unknown.length) reportKeys(keys.unknown);
if (keys.missing.length) console.error(` api-keys.mdx never teaches: ${keys.missing.join(', ')}`);
if (keys.unknown.length || keys.missing.length) {
throw new Error(
`${keys.unknown.length} mentions spell a key prefix cloud does not mint` +
(keys.missing.length ? `, and ${keys.missing.length} key type(s) go untaught` : ''),
);
}
console.log(
`[keys] ${keys.checked} credential literals checked against ` +
keys.keys.map((k) => k.prefix).join(' / '),
);
}
await main().catch((e) => {
+2 -2
View File
@@ -291,7 +291,7 @@ export default function HomePage() {
from hanzoai import Hanzo
client = Hanzo(api_key="hk-...")
client = Hanzo(api_key="sk-...")
r = client.chat.completions.create(
model="zen5",
messages=[{"role": "user", "content": "Hello!"}],
@@ -304,7 +304,7 @@ print(r.choices[0].message.content)`}</code></pre>
<Terminal className="h-3.5 w-3.5" /> curl
</div>
<pre className="p-5 text-sm overflow-x-auto"><code>{`curl https://api.hanzo.ai/v1/chat/completions \\
-H "Authorization: Bearer hk-..." \\
-H "Authorization: Bearer sk-..." \\
-H "Content-Type: application/json" \\
-d '{
"model": "zen5",
@@ -97,7 +97,7 @@ pip install hanzoai
```python
from hanzoai import TrainingClient
client = TrainingClient(api_key="hk-...")
client = TrainingClient(api_key="sk-...")
# Single trace
client.submit_trace(
@@ -85,7 +85,7 @@ Set `stream: true` to receive server-sent events:
```python
from hanzoai import Hanzo
client = Hanzo(api_key="hk-your-key")
client = Hanzo(api_key="sk-your-key")
stream = client.chat.completions.create(
model="zen4",
@@ -38,7 +38,7 @@ curl https://api.hanzo.ai/v1/embeddings \
```python
from hanzoai import Hanzo
client = Hanzo(api_key="hk-your-key")
client = Hanzo(api_key="sk-your-key")
response = client.embeddings.create(
model="zen-embedding",
@@ -54,7 +54,7 @@ for embedding in response.data:
```ts
import Hanzo from '@hanzo/ai'
const client = new Hanzo({ apiKey: 'hk-your-key' })
const client = new Hanzo({ apiKey: 'sk-your-key' })
const response = await client.embeddings.create({
model: 'zen-embedding',
+3 -3
View File
@@ -15,7 +15,7 @@ https://api.hanzo.ai/v1
## Authentication
All API requests require a `HANZO_API_KEY` (prefix: `hk-`) obtained from [console.hanzo.ai](https://console.hanzo.ai).
All API requests require a `HANZO_API_KEY` (prefix: `sk-`) obtained from [console.hanzo.ai](https://console.hanzo.ai).
```bash
Authorization: Bearer $HANZO_API_KEY
@@ -44,7 +44,7 @@ curl https://api.hanzo.ai/v1/chat/completions \
```python
from hanzoai import Hanzo
client = Hanzo(api_key="hk-your-api-key")
client = Hanzo(api_key="sk-your-api-key")
response = client.chat.completions.create(
model="zen4",
@@ -58,7 +58,7 @@ print(response.choices[0].message.content)
```typescript
import Hanzo from "hanzoai";
const client = new Hanzo({ apiKey: "hk-your-api-key" });
const client = new Hanzo({ apiKey: "sk-your-api-key" });
const response = await client.chat.completions.create({
model: "zen4",
+2 -2
View File
@@ -54,7 +54,7 @@ curl https://api.hanzo.ai/v1/messages \
from anthropic import Anthropic
client = Anthropic(
api_key="hk-your-api-key",
api_key="sk-your-api-key",
base_url="https://api.hanzo.ai/v1",
)
@@ -72,7 +72,7 @@ print(message.content[0].text)
import Anthropic from "@anthropic-ai/sdk";
const client = new Anthropic({
apiKey: "hk-your-api-key",
apiKey: "sk-your-api-key",
baseURL: "https://api.hanzo.ai/v1",
});
+1 -1
View File
@@ -53,5 +53,5 @@ it.
1. Create an account at [console.hanzo.ai](https://console.hanzo.ai)
2. Your **$5 free credit** is applied automatically
3. Generate an API key (prefix: `hk-`)
3. Generate an API key (prefix: `sk-`)
4. Start making requests to `https://api.hanzo.ai/v1`
@@ -18,7 +18,7 @@ pip install hanzoai
```python
from hanzoai import Hanzo
client = Hanzo(api_key="hk-your-api-key")
client = Hanzo(api_key="sk-your-api-key")
response = client.chat.completions.create(
model="zen4",
+5 -5
View File
@@ -31,7 +31,7 @@ Zen models are served at `https://api.hanzo.ai/v1`. Get a key from the
```bash
curl https://api.hanzo.ai/v1/chat/completions \
-H "Authorization: Bearer hk-your-api-key" \
-H "Authorization: Bearer sk-your-api-key" \
-H "Content-Type: application/json" \
-d '{"model": "zen5", "messages": [{"role": "user", "content": "Hello, Zen."}]}'
```
@@ -41,7 +41,7 @@ curl https://api.hanzo.ai/v1/chat/completions \
```python
from hanzoai import Hanzo
client = Hanzo(api_key="hk-your-api-key")
client = Hanzo(api_key="sk-your-api-key")
response = client.chat.completions.create(
model="zen5",
@@ -55,7 +55,7 @@ print(response.choices[0].message.content)
```ts
import Hanzo from '@hanzo/ai'
const client = new Hanzo({ apiKey: 'hk-your-api-key' })
const client = new Hanzo({ apiKey: 'sk-your-api-key' })
const response = await client.chat.completions.create({
model: 'zen5',
@@ -83,7 +83,7 @@ from openai import OpenAI
client = OpenAI(
base_url="https://api.hanzo.ai/v1",
api_key="hk-your-api-key",
api_key="sk-your-api-key",
)
response = client.chat.completions.create(
@@ -94,7 +94,7 @@ response = client.chat.completions.create(
Model ids do not carry across vendors — `zen5` is a Hanzo model and resolves
only here.
- **API key prefix**: `hk-`
- **API key prefix**: `sk-`
- **Endpoint**: `api.hanzo.ai/v1`
## All Models
+3 -3
View File
@@ -7,7 +7,7 @@ description: Complete Zen LM model family — 80+ models across text, code, visi
Zen LM by Hanzo AI is a comprehensive model family built using **Zen MoDE (Mixture of Diverse Experts)** — diverse expert specializations fused into a unified, high-performance family.
All API models are accessed at `api.hanzo.ai`. API key prefix: `hk-`. Open-weight models are available on [HuggingFace](https://huggingface.co/zenlm).
All API models are accessed at `api.hanzo.ai`. API key prefix: `sk-`. Open-weight models are available on [HuggingFace](https://huggingface.co/zenlm).
---
@@ -124,7 +124,7 @@ Agent-optimized models for tool use, planning, and autonomous workflows.
```python
from hanzoai import Hanzo
client = Hanzo(api_key="hk-your-api-key")
client = Hanzo(api_key="sk-your-api-key")
response = client.chat.completions.create(
model="zen4",
@@ -136,7 +136,7 @@ print(response.choices[0].message.content)
```typescript
import Hanzo from '@hanzo/ai';
const client = new Hanzo({ apiKey: 'hk-your-api-key' });
const client = new Hanzo({ apiKey: 'sk-your-api-key' });
const response = await client.chat.completions.create({
model: 'zen4',
+1 -1
View File
@@ -35,7 +35,7 @@ A 3D asset generation model that creates meshes, textures, and materials from te
```python
from hanzoai import Hanzo
client = Hanzo(api_key="hk-your-api-key")
client = Hanzo(api_key="sk-your-api-key")
# Coming soon
response = client.models.generate_3d(
@@ -39,7 +39,7 @@ A 32B dense transformer designed for agentic workflows with native tool use, mul
```python
from hanzoai import Hanzo
client = Hanzo(api_key="hk-your-api-key")
client = Hanzo(api_key="sk-your-api-key")
tools = [
{
@@ -36,7 +36,7 @@ An image editing model for inpainting, outpainting, and instruction-based modifi
```python
from hanzoai import Hanzo
client = Hanzo(api_key="hk-your-api-key")
client = Hanzo(api_key="sk-your-api-key")
response = client.images.edit(
model="zen-artist-edit",
@@ -36,7 +36,7 @@ A dedicated image generation model capable of producing high-resolution images a
```python
from hanzoai import Hanzo
client = Hanzo(api_key="hk-your-api-key")
client = Hanzo(api_key="sk-your-api-key")
response = client.images.generate(
model="zen-artist",
@@ -52,7 +52,7 @@ print(tokenizer.decode(outputs[0], skip_special_tokens=True))
```python
from hanzoai import Hanzo
client = Hanzo(api_key="hk-your-api-key")
client = Hanzo(api_key="sk-your-api-key")
response = client.chat.completions.create(
model="zen-code",
@@ -53,7 +53,7 @@ print(tokenizer.decode(outputs[0], skip_special_tokens=True))
```python
from hanzoai import Hanzo
client = Hanzo(api_key="hk-your-api-key")
client = Hanzo(api_key="sk-your-api-key")
response = client.chat.completions.create(
model="zen5-coder-flash",
@@ -53,7 +53,7 @@ print(tokenizer.decode(outputs[0], skip_special_tokens=True))
```python
from hanzoai import Hanzo
client = Hanzo(api_key="hk-your-api-key")
client = Hanzo(api_key="sk-your-api-key")
response = client.chat.completions.create(
model="zen5-coder",
@@ -35,7 +35,7 @@ A specialized model for generating UI/UX designs, graphics, layouts, and visual
```python
from hanzoai import Hanzo
client = Hanzo(api_key="hk-your-api-key")
client = Hanzo(api_key="sk-your-api-key")
# Coming soon
response = client.images.generate(
@@ -35,7 +35,7 @@ A text-to-video generation model producing cinematic-quality video clips from na
```python
from hanzoai import Hanzo
client = Hanzo(api_key="hk-your-api-key")
client = Hanzo(api_key="sk-your-api-key")
# Coming soon
response = client.videos.generate(
@@ -37,7 +37,7 @@ A real-time voice synthesis model designed for ultra-low latency live applicatio
```python
from hanzoai import Hanzo
client = Hanzo(api_key="hk-your-api-key")
client = Hanzo(api_key="sk-your-api-key")
# Coming soon -- streaming API
with client.audio.speech.create(
@@ -37,7 +37,7 @@ A voice synthesis model for text-to-speech, voice cloning, and multi-language du
```python
from hanzoai import Hanzo
client = Hanzo(api_key="hk-your-api-key")
client = Hanzo(api_key="sk-your-api-key")
# Coming soon
response = client.audio.speech.create(
@@ -54,7 +54,7 @@ print(tokenizer.decode(outputs[0], skip_special_tokens=True))
```python
from hanzoai import Hanzo
client = Hanzo(api_key="hk-your-api-key")
client = Hanzo(api_key="sk-your-api-key")
response = client.chat.completions.create(
model="zen-eco",
@@ -45,7 +45,7 @@ curl https://api.hanzo.ai/v1/embeddings \
```python
from hanzoai import Hanzo
client = Hanzo(api_key="hk-your-api-key")
client = Hanzo(api_key="sk-your-api-key")
response = client.embeddings.create(
model="zen-embedding",

Some files were not shown because too many files have changed in this diff Show More