API Keys
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. There are two kinds of key, they are not interchangeable, and the difference is what each one resolves to when the door reads it.
The two types
| Prefix | Type | What it is |
|---|---|---|
sk- | secret | session-equivalent, belongs on a server |
pk- | publishable | org-identifying, safe in a browser bundle |
sk- belongs on a server and pk- may ship in a browser bundle. A key spelled any other way is refused at the door — never guessed at, never
defaulted to the safer-sounding one.
What each one resolves to
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.
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.
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.
They are two rows, not two halves of one key
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.
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.
Creating keys
In the console
- Go to console.hanzo.ai
- Open API Keys
- Create a key and choose its type
- Copy a secret key immediately — it is shown once and never again
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.
Over the API
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.
# 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"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, 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-keyEvery 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/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 | /v1/tasks | api.hanzo.ai/v1/tasks/namespaces |
SDK configuration
All official SDKs accept the key as a constructor parameter:
# Python
from hanzoai import Hanzo
client = Hanzo(api_key="sk-your-key")// TypeScript
import Hanzo from '@hanzo/ai'
const client = new Hanzo({ apiKey: 'sk-your-key' })// Go
import "github.com/hanzoai/go-sdk"
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.
Spend and usage
Spend for the calling credential comes from the billing and analytics APIs — same host, same key:
# Current balance
curl https://api.hanzo.ai/v1/billing/balance \
-H "Authorization: Bearer sk-your-key"
# Itemized metered usage
curl https://api.hanzo.ai/v1/billing/usage \
-H "Authorization: Bearer sk-your-key"{ "balance": 0, "holds": 0, "available": 0 }{
"count": 2000,
"usage": [
{
"amount": 24,
"createdAt": "2026-07-25T21:59:35Z",
"metadata": { "model": "zen-image" },
"transactionId": "use_daff3c10f561635114c7a78e8d0eea5d"
}
]
}For rolled-up request, token and spend counts across a range, use GET https://api.hanzo.ai/v1/analytics/overview?range=30d — see
Analytics.
Rotation
- 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.
- Keep production keys in KMS. Never in source, never
in a committed
.env. - 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.
- One identity per service. Blast radius, and audit logs you can read.
- Watch spend.
/v1/billing/usageor the console. An unfamiliar spend pattern is what a leaked key looks like from the inside.
KMS integration
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:
apiVersion: secrets.lux.network/v1alpha1
kind: KMSSecret
metadata:
name: api-kms-sync
namespace: acme
spec:
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: OpaquecreationPolicy: Orphan means deleting the CR leaves the Secret in place, so
removing the sync does not take a running workload down with it.
Revoking
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.
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.
How is this guide?