Hanzo AI

Authentication

How a request proves who it is — the two Hanzo key types, which one may ship in a browser, and how to create, rotate and revoke them.

Authentication

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 edge reads it.

The two types

PrefixTypeWhat it is
sk-secretsession-equivalent, belongs on a server
pk-publishableorg-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 edge — never guessed at, never defaulted to the safer-sounding one.

What each one resolves to

A secret key resolves to you. The edge 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 edge rather than promised: a pk- presented as a bearer credential is refused outright, no matter which endpoint it arrives at. One pk- covers events, 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 event stream 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

  1. Go to console.hanzo.ai
  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

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/account/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/account/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/account/keys -H "Authorization: Bearer sk-your-key"

# Revoke one type without touching the other.
curl -X DELETE "https://api.hanzo.ai/v1/account/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.

Limiting what a key may reach

A key reaches whatever its holder reaches. For a key that lives in one deployed service that is more authority than the service needs, so a key can be narrowed to a model, a project or a product:

curl -X POST https://api.hanzo.ai/v1/account/keys \
  -H "Authorization: Bearer sk-your-key" \
  -H "Content-Type: application/json" \
  -d '{"type":"secret","limit":["model:zen5","project:acme"]}'

The entries are kind:name, and kind:* is the whole kind. Three kinds are enforced — model, project and product — and a kind outside that set is refused when you create the key rather than stored. A limit nothing consults would be worse than no limit: it would read as a restriction everywhere you look and be one nowhere.

A limit only ever narrows. It cannot grant a key reach its holder does not have, so the worst a mistake costs is a call that should have worked. That is also why an omitted limit means unrestricted: every key issued before limits existed carries none, and a default that restricted would have revoked them all.

Restriction is per kind. A key limited to a project is not thereby limited to zero models — naming one kind says nothing about the others, so adding a limit later cannot silently switch off something that was working.

The limit belongs to the credential, not the person, which has two consequences worth stating plainly. Your session and your deploy key can carry different limits at the same time. And an admin's limited key is still limited: a restriction you could shed by being an admin would not be one.

GET /v1/account/keys reports each key's limit, so you can see what a key reaches without minting a new one to find out.

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-key

Every service answers on one host, api.hanzo.ai, under /v1/<service> — the same key works across all of them:

ServicePathExample
AI/v1/chat/completions, /v1/models, /v1/embeddingsapi.hanzo.ai/v1/chat/completions
Search/v1/searchapi.hanzo.ai/v1/search
Secrets/v1/kmsapi.hanzo.ai/v1/kms/secrets
Deploys/v1/platformapi.hanzo.ai/v1/platform/projects
Events/v1/eventapi.hanzo.ai/v1/event/overview
Observability/v1/o11yapi.hanzo.ai/v1/o11y/status?product=cloud
Tasks/v1/tasksapi.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 event 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/event/overview?range=30d — see Events.

Rotation

  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. 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

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: Opaque

creationPolicy: 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/account/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?