Catalog API
Package discovery, install/uninstall, publishing, collections, and registry management. This is also how agents/workflows are provisioned — there is no direct "create agent" API; you install an AgentTemplate/WorkflowTemplate package via /catalog/install.
Endpoints
| Method | Path | Action | Description |
|---|---|---|---|
| GET | /catalog/taxonomy | CatalogRead | Compiled-in category taxonomy tree |
| GET | /catalog/search | CatalogRead | Search packages |
| GET | /catalog/packages/{name} | CatalogRead | Package details |
| GET | /catalog/packages/{name}/versions | CatalogRead | List versions |
| POST | /catalog/install | CatalogInstall | Install (and activate) a package |
| POST | /catalog/uninstall | CatalogManage | Uninstall a package |
| GET | /catalog/installed | CatalogRead | List installed packages |
| POST | /catalog/publish | PackagePublish | Publish a package archive |
| POST | /catalog/yank | CatalogManage | Yank (deprecate) a version |
| GET | /catalog/registries | CatalogRead | List configured registries |
| POST | /catalog/registries | CatalogManage | Add a registry |
| GET | /catalog/registries/{name} | CatalogRead | Get a registry |
| PUT | /catalog/registries/{name} | CatalogManage | Update a registry |
| DELETE | /catalog/registries/{name} | CatalogManage | Remove a registry |
| POST | /catalog/registries/{name}/test | CatalogManage | Test connectivity |
| GET | /catalog/collections | CatalogRead | List collections |
| POST | /catalog/collections | CatalogManage | Create a collection (proxied to registry) |
| GET | /catalog/collections/{name} | CatalogRead | Get collection details |
| PUT | /catalog/collections/{name} | CatalogManage | Update a collection |
| DELETE | /catalog/collections/{name} | CatalogManage | Delete a collection |
| POST | /catalog/collections/{name}/install | CatalogInstall | Install every package in a collection |
CatalogInstall is admin/operator/developer; CatalogManage (uninstall, registries, collections write, yank) is admin/operator only; CatalogRead is open to everyone including service/auditor. PackagePublish (admin/operator/developer) is checked in the handler for catalog_publish, and CatalogManage for catalog_yank — despite an older code comment above the route registrations in routes.rs claiming these two are "intentionally unprotected at the daemon level," the current handlers do call check_role(); treat the handler code, not the comment, as ground truth.
GET /catalog/taxonomy
Serves the compiled-in category taxonomy (same tree the registry's GET /taxonomy uses) so authors can browse valid [package.metadata] categories paths without a reachable registry.
curl -H "Api-Version: v1" -H "Authorization: Bearer hzn_sk_..." \
http://localhost:4200/catalog/taxonomy
Response (200)
{
"version": "1.0.0",
"domains": [
{"id": "claims", "name": "Claims", "description": "...", "subdomains": [{"id": "claims.fnol", "name": "FNOL", "description": "..."}]}
]
}
GET /catalog/search
curl -H "Api-Version: v1" -H "Authorization: Bearer hzn_sk_..." \
"http://localhost:4200/catalog/search?q=claims&domain=claims®ion=us&page=1&per_page=20"
Query Parameters
| Param | Type | Description |
|---|---|---|
q | string | Free-text query |
domain | string | Taxonomy domain filter |
region | string | Region filter |
page | integer | 1-indexed, default 1 |
per_page | integer | Default 20 |
Note: type/package-type filtering exists in the underlying SearchQuery struct but is not currently exposed as a query param on this route (always None).
Response (200) — SearchResults
{
"packages": [
{
"name": "claims-intake",
"version": "1.2.0",
"package_type": "Competency",
"description": "FNOL processing for auto/property",
"license": "MIT",
"downloads": 412,
"categories": ["claims.fnol"],
"regions": ["us", "uk"],
"signed": true
}
],
"total": 1,
"page": 1,
"per_page": 20
}
author is omitted entirely (issue #619 — authorship is now the authenticated publisher, not self-declared free text).
Error (502 — registry unreachable)
{"error": {"category": "CatalogError", "message": "catalog registry unreachable: ..."}}
GET /catalog/packages/{name}
Response (200) — PackageInfo
Same shape as one entry of /catalog/search's packages array.
Error (404)
{"error": {"category": "NotFound", "message": "package 'claims-intake' not found"}}
GET /catalog/packages/{name}/versions
Response (200) — array of PackageVersionInfo
[
{
"version": "1.2.0",
"published_at": "2026-05-01T00:00:00Z",
"content_hash": "sha256:...",
"size_bytes": 48213,
"signed": true,
"dependencies": ["document-ocr@^1.0"],
"yanked": false,
"publisher_id": "insurance-corp",
"registry_signature": "...",
"registry_key_id": "...",
"signed_at": "2026-05-01T00:00:05Z",
"release_notes": "Initial release"
}
]
POST /catalog/install
Downloads and activates a package in one call. Activation errors (post-download) surface as 500 with the partial install result attached, not a silent failure.
curl -X POST -H "Api-Version: v1" -H "Authorization: Bearer hzn_sk_..." \
-H "Content-Type: application/json" \
-d '{"package": "claims-intake", "version": "1.2.0"}' \
http://localhost:4200/catalog/install
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
package | string | Yes | Package name |
version | string | No | Specific version (latest if omitted) |
Response (200) — InstalledPackage
{
"name": "claims-intake",
"version": "1.2.0",
"package_type": "Competency",
"installed_at": "2026-06-04T10:00:00Z",
"content_hash": "sha256:...",
"already_installed": false,
"resource_id": "claims-intake",
"resource_state": "registered"
}
resource_id/resource_state meaning depends on package_type — for AgentTemplate it's the kernel agent UUID and its lifecycle state ("running", "created", ...); for Competency/Skill/Integration it's the id and "registered"; for WorkflowTemplate it's the workflow store UUID and "hydrated". region_warning appears when the package's declared regions don't include the carrier's home_region (advisory only, install still proceeds).
Response (500 — activation failed after download)
{
"error": {"category": "ActivationError", "message": "..."},
"installed": { "...": "partial InstalledPackage with activation_error set" }
}
POST /catalog/uninstall
curl -X POST -H "Api-Version: v1" -H "Authorization: Bearer hzn_sk_..." \
-H "Content-Type: application/json" \
-d '{"package": "claims-intake"}' \
http://localhost:4200/catalog/uninstall
Response (200)
{"status": "uninstalled", "package": "claims-intake"}
GET /catalog/installed
{"packages": [ /* InstalledPackage[] */ ], "total": 3}
POST /catalog/publish
Publishes a local .hpkg archive to a registry. The archive path is validated (must be relative, no .. traversal) and its package name is read from the archive's own MANIFEST.toml — the request does not pass name/version directly.
curl -X POST -H "Api-Version: v1" -H "Authorization: Bearer hzn_sk_..." \
-H "Content-Type: application/json" \
-d '{"archive_path": "dist/claims-intake-1.2.0.hpkg", "registry": "internal"}' \
http://localhost:4200/catalog/publish
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
archive_path | string | Yes | Relative path to the .hpkg archive, resolved server-side |
registry | string | No | Registry name (defaults to the daemon's configured publish target) |
Response (201) — PublishResult
{
"name": "claims-intake",
"version": "1.2.0",
"content_hash": "sha256:...",
"registry": "internal",
"published_at": "2026-06-04T10:00:00Z"
}
POST /catalog/yank
curl -X POST -H "Api-Version: v1" -H "Authorization: Bearer hzn_sk_..." \
-H "Content-Type: application/json" \
-d '{"package": "claims-intake", "version": "1.0.0"}' \
http://localhost:4200/catalog/yank
Response (200) — YankResult
{"name": "claims-intake", "version": "1.0.0", "yanked": true}
GET /catalog/registries
{
"registries": [
{"name": "default", "url": "https://catalog.hoziron.com", "priority": 100, "enabled": true, "require_signatures": true, "status": "Unknown"}
]
}
status is always "Unknown" on the list endpoint (RegistryConnectivity — Reachable/Unreachable/Unknown) — call POST /catalog/registries/{name}/test for a live check.
POST /catalog/registries
curl -X POST -H "Api-Version: v1" -H "Authorization: Bearer hzn_sk_..." \
-H "Content-Type: application/json" \
-d '{
"name": "internal",
"url": "https://packages.internal.company.com",
"priority": 50,
"auth_token_env": "INTERNAL_REGISTRY_TOKEN"
}' \
http://localhost:4200/catalog/registries
priority defaults to 100, enabled/require_signatures default to true if omitted.
Response (201)
{"status": "added", "registry": {"name": "internal", "url": "https://packages.internal.company.com", "priority": 50, "enabled": true}}
Response (409 — duplicate name)
{"error": {"category": "Conflict", "message": "..."}}
PUT /catalog/registries/{name}
Body is a RegistryUpdate — every field optional, only supplied fields change:
curl -X PUT -H "Api-Version: v1" -H "Authorization: Bearer hzn_sk_..." \
-H "Content-Type: application/json" \
-d '{"priority": 10, "enabled": false}' \
http://localhost:4200/catalog/registries/internal
Response (200)
{"status": "updated", "registry": "internal"}
DELETE /catalog/registries/{name}
Response (200)
{"status": "removed", "registry": "internal"}
POST /catalog/registries/{name}/test
Response (200) — RegistryTestResult
{"reachable": true, "authenticated": true, "latency_ms": 45, "server_version": "1.4.0"}
GET /catalog/collections
{"collections": [{"name": "claims-starter", "display_name": "Claims Starter Pack", "description": "...", "package_count": 4, "tags": ["insurance", "claims"], "featured": true}], "total": 1}
POST /catalog/collections/{name}/install
Installs every package in a collection in one call.
curl -X POST -H "Api-Version: v1" -H "Authorization: Bearer hzn_sk_..." \
http://localhost:4200/catalog/collections/claims-starter/install
Response (200)
{"status": "installed", "collection": "claims-starter", "packages": [ /* per-package install results */ ]}
Related
- skills.md, competencies.md — packages of these types, installable individually via
/skills/installand/competencies/installas well as through the catalog - agents.md — agents are activated as a side effect of installing an
AgentTemplatepackage