Get kit health
Nine-field kit health, unauthenticated by design.
The Hoody bot kit exposes a small HTTP surface for controlling Hoody from a chat app such as Telegram. It is chat-app agnostic, with Telegram as the first supported transport. This page documents the three bot:system operations: the unauthenticated health check, the verified chat manifest, and the key-rotation endpoint for re-encrypting stored credentials. For the chat-app surface itself and how to wire a chat app to the kit, see the bot kit, chat access concepts, and the chat control walkthrough.
Get kit health
Nine-field kit health, unauthenticated by design.
Get chat manifest
The verified chat manifest this build is pinned to, byte-for-byte as baked.
Rotate sealed columns
Re-encrypt every sealed column under a new kit key.
The bot service runs inside the container and is reachable at the per-container public service URL:
https://{projectId}-{containerId}-bot-1.{server}.containers.hoody.com
All three bot:system operations live under /api/v1/bot/... on that host. Use the container’s public service URL: Hoody Kit programs are reached through their URLs only, and a request refused by the Source IP Guard gets 403 forbidden.
GET /api/v1/bot/health is unauthenticated by design. It is the only public endpoint in the bot surface, intended for liveness probes and for the CLI to discover the kit’s pinned manifest hash. A bare /health returns 404; the route must include the /api/v1 prefix.
Every other bot route requires Authorization: Bearer <your own Hoody token> AND container ownership. The kit validates the bearer against the kit itself, then reads the container, the caller, and the container’s project. Only the project’s owner is admitted. A container you can only read is refused, including one readable to an administrator. Anything that does not pass returns 401 with one of the admission error codes listed below. owner_unresolved means the project could not be read or carried no owner id, not that the container record lacked one.
A request that does not come through the kit’s URL is refused by the Source IP Guard with 403 forbidden. Call the kit through the container’s public service URL shown above.
| Error Code | Title | Description | Resolution |
|---|---|---|---|
bearer_missing | Bearer missing | The Authorization header is absent from the request | Send the request with Authorization: Bearer <token> |
bearer_malformed | Bearer malformed | The Authorization header is present but is not a valid Bearer credential | Send Authorization: Bearer <token> with a single space and the raw token |
bearer_rejected | Bearer rejected | The bearer was checked against the kit and refused | Mint a fresh Hoody token, then retry |
container_unreadable | Container unreadable | The container record could not be loaded to evaluate ownership | Retry once the database is reachable; if the error persists, the kit is refusing the container |
not_container_owner | Not container owner | The caller is authenticated but is not the owner of the container | Use the token of the user that owns the container, or rotate the container’s ownership |
owner_unresolved | Owner unresolved | The project for the container could not be read or carried no owner id | The container has no resolvable owner; contact the project administrator |
identity_unavailable | Identity unavailable | The caller identity could not be resolved from the bearer | Retry; if it persists, the kit cannot resolve identities in this build |
upstream_unavailable | Upstream unavailable | The dependency the kit reads (database, cache, identity service) is unreachable | Retry after the dependency is reachable |
GET /api/v1/bot/healthNine-field kit health; unauthenticated by design. open_by_default is null until the self-probe has resolved; it is never reported as safe by default.
This endpoint takes no parameters.
curl https://{projectId}-{containerId}-bot-1.{server}.containers.hoody.com/api/v1/bot/healthimport { HoodyClient } from 'hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://{projectId}-{containerId}-bot-1.{server}.containers.hoody.com', token: process.env.HOODY_TOKEN });
await client.bot.kit.getHealth();hoody bot health{ "status": "ok", "version": "1.0.0", "uptime_s": 3600, "mode": "single", "spec_hash": "1a2b3c4d5e6f7081928374a5b6c7d8e9f0a1b2c3d4e5f67890a1b2c3d4e5f6a7", "overlay_hash": "9b71d224bd62f3785d96d46ad3ea3d73319bfbc2890caadae2dff72519673ca7", "manifest_hash": "7d865e959b2466918c9863afca942d0fb89d7c9ac0c99bafc3749504ded97730", "open_by_default": false, "polling": { "registrations": 1, "active": 0, "last_error_code": null, "last_error_at": null }}| Field | Type | Description |
|---|---|---|
status | string | Always ok when the kit is serving health |
version | string | The kit version string |
uptime_s | integer | Seconds since the kit process started |
mode | string | Either single (one bot registration) or multi (more than one) |
spec_hash | string | RFC 8785 digest of the open spec this build was generated from, or null if not yet computed |
overlay_hash | string | Digest of the chat-mappings overlay (kept for wire compatibility) |
manifest_hash | string | The same digest as the served manifest’s manifest_sha256 |
open_by_default | boolean | True if the kit self-probe resolved and the bot opens to anyone by default; null until the probe completes |
polling.registrations | integer | Number of chat-app registrations the kit is tracking |
polling.active | integer | Number of pollers currently running |
polling.last_error_code | string | One of network, auth, conflict, rate_limited, unknown, or null if no error has occurred |
polling.last_error_at | integer | Unix timestamp of the last poller error, or null |
{ "error": { "code": "internal_error", "message": "the kit could not complete this request" }}GET /api/v1/bot/manifestThe verified chat-manifest.json. The kit serves it only after recomputing its RFC 8785 (JCS) digest and matching it against the digest packaging recorded, so the bytes here are the bytes that were pinned; hoody bot manifest get --verify recomputes the same digest and compares it with the value baked into the CLI and with health’s manifest_hash. 503 manifest_unavailable when no manifest is baked into this build, when the baked one was refused at boot, or when redaction at the render boundary would alter the bytes; the kit serves the verified document or nothing, never altered content under an unaltered digest.
The document declares 774 commands and the kit offers 768 of them; the six it does not offer require a file-upload argument (four) or a WebSocket stream (two).
This endpoint takes no parameters.
curl https://{projectId}-{containerId}-bot-1.{server}.containers.hoody.com/api/v1/bot/manifestimport { HoodyClient } from 'hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://{projectId}-{containerId}-bot-1.{server}.containers.hoody.com', token: process.env.HOODY_TOKEN });
await client.bot.kit.getManifest();hoody bot manifest gethoody bot manifest get --verify{ "schema_version": "1", "sources": { "spec_sha256": { "api": "8e1a2b3c4d5e6f7081928374a5b6c7d8e9f0a1b2c3d4e5f67890a1b2c3d4e5f6", "agent": "9f2b3c4d5e6f7081928374a5b6c7d8e9f0a1b2c3d4e5f67890a1b2c3d4e5f6a7", "bot": "a3c4d5e6f7081928374a5b6c7d8e9f0a1b2c3d4e5f67890a1b2c3d4e5f6a7b8a", "browser": "b4d5e6f7081928374a5b6c7d8e9f0a1b2c3d4e5f67890a1b2c3d4e5f6a7b8c9a", "code": "c5e6f7081928374a5b6c7d8e9f0a1b2c3d4e5f67890a1b2c3d4e5f6a7b8c9d0a", "cron": "d6f7081928374a5b6c7d8e9f0a1b2c3d4e5f67890a1b2c3d4e5f6a7b8c9d0e1a", "curl": "e7081928374a5b6c7d8e9f0a1b2c3d4e5f67890a1b2c3d4e5f6a7b8c9d0e1f2a", "daemon": "f81928374a5b6c7d8e9f0a1b2c3d4e5f67890a1b2c3d4e5f6a7b8c9d0e1f2a3a", "display": "0829374a5b6c7d8e9f0a1b2c3d4e5f67890a1b2c3d4e5f6a7b8c9d0e1f2a3b4a", "egress": "19374a5b6c7d8e9f0a1b2c3d4e5f67890a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5a", "exec": "274a5b6c7d8e9f0a1b2c3d4e5f67890a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6a", "files": "35a5b6c7d8e9f0a1b2c3d4e5f67890a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7", "notes": "46a6b7c8d9e0f1a2b3c4d5e6f7081928374a5b6c7d8e9f0a1b2c3d4e5f6a7b8a", "notifications": "57b7c8d9e0f1a2b3c4d5e6f7081928374a5b6c7d8e9f0a1b2c3d4e5f6a7b8c9a", "pipe": "68c8d9e0f1a2b3c4d5e6f7081928374a5b6c7d8e9f0a1b2c3d4e5f6a7b8c9d0a", "proxyLogs": "79d9e0f1a2b3c4d5e6f7081928374a5b6c7d8e9f0a1b2c3d4e5f6a7b8c9d0e1a", "run": "8ae0f1a2b3c4d5e6f7081928374a5b6c7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a", "sqlite": "9bf1a2b3c4d5e6f7081928374a5b6c7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3a", "terminal": "acf2b3c4d5e6f7081928374a5b6c7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4a", "tunnel": "bdf3c4d5e6f7081928374a5b6c7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5a", "watch": "cef4d5e6f7081928374a5b6c7d8e9f0a1b2c3d4e5f67890a1b2c3d4e5f6a7b8a" }, "chat_mappings_sha256": "9b71d224bd62f3785d96d46ad3ea3d73319bfbc2890caadae2dff72519673ca7" }, "counts": { "universe": 780, "commands": 774, "excluded_operations": 6, "shortcuts": 12, "pickers_missing": 0, "danger": 3, "exec_class": 5 }, "manifest_sha256": "7d865e959b2466918c9863afca942d0fb89d7c9ac0c99bafc3749504ded97730", "commands": [ { "id": "proxyLogs::getLogStats", "cli_mapping_key": "proxy-logs:get-log-stats", "sdk_operation_id": "getLogStats", "routes": [ { "method": "get", "path": "/api/v1/containers/{containerId}/proxy/logs/stats" } ] } ], "shortcuts": [], "builtins": [], "exclusions": [], "registered_commands": { "all_private_chats": [], "all_group_chats": [] }}… (774 commands)
| Field | Type | Description |
|---|---|---|
schema_version | string | The manifest major; this kit serves major 1 only |
sources.spec_sha256 | object | Map from open-spec namespace to the RFC 8785 digest of that namespace’s spec, exactly 64 lowercase hex characters per value |
sources.chat_mappings_sha256 | string | Digest of the chat-mappings overlay, exactly 64 lowercase hex characters |
counts.universe | integer | Total operations in the open spec; equals commands + excluded_operations |
counts.commands | integer | Number of entries in the commands array |
counts.excluded_operations | integer | Operations in the universe that the kit does not surface as chat commands |
counts.shortcuts | integer | Number of entries in the shortcuts array |
counts.pickers_missing | integer | Operations that need a picker but have none defined |
counts.danger | integer | Operations flagged with the danger risk class |
counts.exec_class | integer | Operations flagged with the exec risk class |
manifest_sha256 | string | JCS digest of this document with this field removed; recomputed by the kit at boot |
commands | array | The full command surface, one entry per namespace::operation |
shortcuts | array | Shortcut bindings that map a chat command to a base command |
builtins | array | Built-in chat commands the kit always offers |
exclusions | array | Operations the kit considered and chose not to expose |
registered_commands.all_private_chats | array | Command names registered for all private chats, up to 100 entries |
registered_commands.all_group_chats | array | Command names registered for all group chats, up to 100 entries |
{ "error": { "code": "internal_error", "message": "management request refused by the kit" }}{ "error": { "code": "manifest_unavailable", "message": "no chat manifest is baked into this build" }}{ "error": { "code": "internal_error", "message": "management request refused by the kit" }}POST /api/v1/bot/kit/keys/rotateRe-encrypts every sealed column under a new kit.key. The new key is published beside the old one as kit.key.next, the re-encryption and a key-generation marker commit in one transaction, and only then is the new key promoted over kit.key, so an interrupted rotation is completed or rolled back at the next start rather than losing every stored credential. Refused while a poller is running unless force=true.
| Name | In | Type | Required | Description |
|---|---|---|---|---|
force | query | string | No | Rotate even though a poller is running. Without it an active poller refuses the rotation. Accepted values: true, false. |
| Error Code | Title | Description | Resolution |
|---|---|---|---|
keys_rotate_refused | Keys rotation refused | A poller is currently active; the kit will not rotate under a running poller unless force=true is passed | Stop the active poller, or call the endpoint again with ?force=true |
keys_rotate_failed | Keys rotation failed | The re-encryption or the new key commit did not complete; the database was rolled back to the previous key | Inspect the kit logs, fix the underlying error, then retry |
invalid_query | Invalid query | The force parameter is not one of the allowed values | Pass force=true or force=false, or omit it entirely |
query_ambiguous | Query ambiguous | The force parameter was supplied more than once with conflicting values | Send force at most once per request |
curl -X POST 'https://{projectId}-{containerId}-bot-1.{server}.containers.hoody.com/api/v1/bot/kit/keys/rotate?force=true'import { HoodyClient } from 'hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://{projectId}-{containerId}-bot-1.{server}.containers.hoody.com', token: process.env.HOODY_TOKEN });
await client.bot.kit.rotateKeys({ force: 'true' });hoody bot keys rotatehoody bot keys rotate --force{ "rotated_rows": 1, "generation": 2, "columns": [ { "table": "registrations", "column": "token_ciphertext", "rows": 1 }, { "table": "users", "column": "parent_token_ciphertext", "rows": 0 }, { "table": "users", "column": "leaf_token_ciphertext", "rows": 0 }, { "table": "users", "column": "pasted_token_ciphertext", "rows": 0 } ], "active_pollers": 0, "forced": false}| Field | Type | Description |
|---|---|---|
rotated_rows | integer | Total rows touched across all sealed columns |
generation | integer | The key generation the database now carries; the new key was published as kit.key.next, committed with this number, and only then promoted over kit.key |
columns | array | One entry per sealed column that was re-encrypted |
columns[].table | string | The table holding the sealed column |
columns[].column | string | The sealed column name |
columns[].rows | integer | Number of rows re-encrypted in that column |
active_pollers | integer | Number of pollers running at the time of rotation |
forced | boolean | True if force=true was passed and a poller was active |
{ "error": { "code": "invalid_query", "message": "management request refused by the kit" }}| Error Code | Title | Description | Resolution |
|---|---|---|---|
invalid_query | Invalid query | The force parameter is not one of the allowed values | Pass force=true or force=false, or omit it entirely |
query_ambiguous | Query ambiguous | The force parameter was supplied more than once with conflicting values | Send force at most once per request |
{ "error": { "code": "keys_rotate_refused", "message": "management request refused by the kit" }}| Error Code | Title | Description | Resolution |
|---|---|---|---|
keys_rotate_refused | Keys rotation refused | A poller is currently active; the kit will not rotate under a running poller unless force=true is passed | Stop the active poller, or call the endpoint again with ?force=true |
{ "error": { "code": "keys_rotate_failed", "message": "management request refused by the kit" }}| Error Code | Title | Description | Resolution |
|---|---|---|---|
keys_rotate_failed | Keys rotation failed | The re-encryption or the new key commit did not complete; the database was rolled back to the previous key | Inspect the kit logs, fix the underlying error, then retry |
internal_error | Internal error | An unexpected error occurred while preparing the rotation | Inspect the kit logs and retry; if it persists, the kit is in an unrecoverable state for rotation |
{ "error": { "code": "admission_unavailable", "message": "this admission class is not wired in this build" }}| Error Code | Title | Description | Resolution |
|---|---|---|---|
admission_unavailable | Admission unavailable | The admission class this route belongs to is not wired into this build | Retry shortly; the admission class becomes available again once its dependency is reachable |
{ "error": { "code": "internal_error", "message": "management request refused by the kit" }}