Every route mounts under /v1/skills, the address api.hanzo.ai routes to this API; auth, personas, users, tokens, upload, integrations, search and whoami are sub-resources and RESERVED keeps them from being slugs. The web, the OG renderers, the CLI's default registry, /.well-known/bothub.json and openapi.json name api.hanzo.ai. Sign-in calls back to api.hanzo.ai/v1/skills/auth/callback and hands the token over in the fragment. The store starts again. docker-entrypoint.sh read Base's port from BASE_PORT, which Kubernetes sets to tcp://<ip>:<port> in a namespace holding a Service named base; Base now listens on 127.0.0.1:8090, fixed. The collections migration is rewritten for the Base the Dockerfile pins (1.5.91): flat field options, declared created/updated, and the users collection every Base already has, extended rather than created twice. Reads are anonymous against public rules, since Base takes IAM credentials only and no longer serves password sign-in; writes answer 503 saying so. The catalogue migration, generated by scripts/catalog.mjs, publishes the 83 skills in hanzoai/bot@8be8f1dc7b. /health reports the store, so the probes restart a pod whose Base is gone. cache.ts decides caching once: an anonymous 2xx GET is public for a minute (five at the edge) with a weak ETag and 304s; a credential, a write or an error is private, no-store. Filter values go through lit(). Embeddings go through api.hanzo.ai and are skipped without a key. k8s/ was a second declaration of the deployment; universe is the one.
5.8 KiB
summary, read_when
| summary | read_when | ||
|---|---|---|---|
| HTTP API reference (public + CLI endpoints + auth). |
|
HTTP API
Base URL: https://api.hanzo.ai (default).
Every path is under /v1/skills/..., served by the API in api/src. Nothing is served under /api.
OpenAPI: /openapi.json.
Rate limits
Enforced per IP + per API key:
- Read: 120/min per IP, 600/min per key
- Write: 30/min per IP, 120/min per key
- Download: 20/min per IP, 120/min per key (
/v1/skills/download)
Headers:
X-RateLimit-Limit,X-RateLimit-Remaining,X-RateLimit-Reset,Retry-After(when limited)
IP source:
- Uses
cf-connecting-ip(Cloudflare) for client IP by default. - Set
TRUST_FORWARDED_IPS=trueto opt in tox-real-ip,x-forwarded-for, orfly-client-ip(non-Cloudflare deployments).
Public endpoints (no auth)
GET /v1/skills/search
Query params:
q(required): query stringlimit(optional): integerhighlightedOnly(optional):trueto filter to highlighted skills
Response:
{ "results": [{ "score": 0.123, "slug": "gifgrep", "displayName": "GifGrep", "summary": "…", "version": "1.2.3", "updatedAt": 1730000000000 }] }
Notes:
- Results are returned in relevance order (embedding similarity + exact slug/name token boosts + popularity prior from downloads).
GET /v1/skills
Query params:
limit(optional): integer (1–200)cursor(optional): pagination cursor (only forsort=updated)sort(optional):updated(default),downloads,stars(alias:rating),installsCurrent(alias:installs),installsAllTime,trending
Notes:
trendingranks by installs in the last 7 days (telemetry-based).
Response:
{ "items": [{ "slug": "gifgrep", "displayName": "GifGrep", "summary": "…", "tags": { "latest": "1.2.3" }, "stats": {}, "createdAt": 0, "updatedAt": 0, "latestVersion": { "version": "1.2.3", "createdAt": 0, "changelog": "…" } }], "nextCursor": null }
GET /v1/skills/{slug}
Response:
{ "skill": { "slug": "gifgrep", "displayName": "GifGrep", "summary": "…", "tags": { "latest": "1.2.3" }, "stats": {}, "createdAt": 0, "updatedAt": 0 }, "latestVersion": { "version": "1.2.3", "createdAt": 0, "changelog": "…" }, "owner": { "handle": "steipete", "displayName": "Peter", "image": null } }
GET /v1/skills/{slug}/versions
Query params:
limit(optional): integercursor(optional): pagination cursor
GET /v1/skills/{slug}/versions/{version}
Returns version metadata + files list.
GET /v1/skills/{slug}/file
Returns raw text content.
Query params:
path(required)version(optional)tag(optional)
Notes:
- Defaults to latest version.
- File size limit: 200KB.
GET /v1/skills/resolve
Used by the CLI to map a local fingerprint to a known version.
Query params:
slug(required)hash(required): 64-char hex sha256 of the bundle fingerprint
Response:
{ "slug": "gifgrep", "match": { "version": "1.2.2" }, "latestVersion": { "version": "1.2.3" } }
GET /v1/skills/download
Downloads a zip of a skill version.
Query params:
slug(required)version(optional): semver stringtag(optional): tag name (e.g.latest)
Notes:
- If neither
versionnortagis provided, the latest version is used. - Soft-deleted versions return
410. - Download stats are counted as unique identities per hour (
userIdwhen API token is valid, otherwise IP).
Auth endpoints (Bearer token)
All endpoints require:
Authorization: Bearer clh_...
GET /v1/skills/whoami
Validates token and returns the user handle.
POST /v1/skills
Publishes a new version.
- Preferred:
multipart/form-datawithpayloadJSON +files[]blobs. - JSON body with
files(storageId-based) is also accepted.
DELETE /v1/skills/{slug} / POST /v1/skills/{slug}/undelete
Soft-delete / restore a skill (moderator/admin only).
Status codes:
200: ok401: unauthorized403: forbidden404: skill/user not found500: internal server error
POST /v1/skills/users/ban
Ban a user and hard-delete owned skills (moderator/admin only).
Body:
{ "handle": "user_handle", "reason": "optional ban reason" }
or
{ "userId": "users_...", "reason": "optional ban reason" }
Response:
{ "ok": true, "alreadyBanned": false, "deletedSkills": 3 }
POST /v1/skills/users/role
Change a user role (admin only).
Body:
{ "handle": "user_handle", "role": "moderator" }
or
{ "userId": "users_...", "role": "admin" }
Response:
{ "ok": true, "role": "moderator" }
GET /v1/skills/users
List or search users (admin only).
Query params:
q(optional): search queryquery(optional): alias forqlimit(optional): max results (default 20, max 200)
Response:
{
"items": [
{
"userId": "users_...",
"handle": "user_handle",
"displayName": "User",
"name": "User",
"role": "moderator"
}
],
"total": 1
}
POST /v1/skills/stars/{slug} / DELETE /v1/skills/stars/{slug}
Add/remove a star (highlights). Both endpoints are idempotent.
Responses:
{ "ok": true, "starred": true, "alreadyStarred": false }
{ "ok": true, "unstarred": true, "alreadyUnstarred": false }
Registry discovery (/.well-known/bothub.json)
The CLI can discover registry/auth settings from the site:
/.well-known/bothub.json(JSON, preferred)/.well-known/bothub.json(legacy)
Schema:
{ "apiBase": "https://api.hanzo.ai", "authBase": "https://hub.hanzo.bot", "registry": "https://api.hanzo.ai", "minCliVersion": "0.1.0" }
If you self-host, serve this file (or set BOTHUB_REGISTRY explicitly; legacy CLAWDHUB_REGISTRY).