Skip to content
Hoody.com

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 CodeTitleDescriptionResolution
bearer_missingBearer missingThe Authorization header is absent from the requestSend the request with Authorization: Bearer <token>
bearer_malformedBearer malformedThe Authorization header is present but is not a valid Bearer credentialSend Authorization: Bearer <token> with a single space and the raw token
bearer_rejectedBearer rejectedThe bearer was checked against the kit and refusedMint a fresh Hoody token, then retry
container_unreadableContainer unreadableThe container record could not be loaded to evaluate ownershipRetry once the database is reachable; if the error persists, the kit is refusing the container
not_container_ownerNot container ownerThe caller is authenticated but is not the owner of the containerUse the token of the user that owns the container, or rotate the container’s ownership
owner_unresolvedOwner unresolvedThe project for the container could not be read or carried no owner idThe container has no resolvable owner; contact the project administrator
identity_unavailableIdentity unavailableThe caller identity could not be resolved from the bearerRetry; if it persists, the kit cannot resolve identities in this build
upstream_unavailableUpstream unavailableThe dependency the kit reads (database, cache, identity service) is unreachableRetry after the dependency is reachable

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

Terminal window
curl https://{projectId}-{containerId}-bot-1.{server}.containers.hoody.com/api/v1/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
}
}
FieldTypeDescription
statusstringAlways ok when the kit is serving health
versionstringThe kit version string
uptime_sintegerSeconds since the kit process started
modestringEither single (one bot registration) or multi (more than one)
spec_hashstringRFC 8785 digest of the open spec this build was generated from, or null if not yet computed
overlay_hashstringDigest of the chat-mappings overlay (kept for wire compatibility)
manifest_hashstringThe same digest as the served manifest’s manifest_sha256
open_by_defaultbooleanTrue if the kit self-probe resolved and the bot opens to anyone by default; null until the probe completes
polling.registrationsintegerNumber of chat-app registrations the kit is tracking
polling.activeintegerNumber of pollers currently running
polling.last_error_codestringOne of network, auth, conflict, rate_limited, unknown, or null if no error has occurred
polling.last_error_atintegerUnix timestamp of the last poller error, or null

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

Terminal window
curl https://{projectId}-{containerId}-bot-1.{server}.containers.hoody.com/api/v1/bot/manifest
{
"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)

FieldTypeDescription
schema_versionstringThe manifest major; this kit serves major 1 only
sources.spec_sha256objectMap from open-spec namespace to the RFC 8785 digest of that namespace’s spec, exactly 64 lowercase hex characters per value
sources.chat_mappings_sha256stringDigest of the chat-mappings overlay, exactly 64 lowercase hex characters
counts.universeintegerTotal operations in the open spec; equals commands + excluded_operations
counts.commandsintegerNumber of entries in the commands array
counts.excluded_operationsintegerOperations in the universe that the kit does not surface as chat commands
counts.shortcutsintegerNumber of entries in the shortcuts array
counts.pickers_missingintegerOperations that need a picker but have none defined
counts.dangerintegerOperations flagged with the danger risk class
counts.exec_classintegerOperations flagged with the exec risk class
manifest_sha256stringJCS digest of this document with this field removed; recomputed by the kit at boot
commandsarrayThe full command surface, one entry per namespace::operation
shortcutsarrayShortcut bindings that map a chat command to a base command
builtinsarrayBuilt-in chat commands the kit always offers
exclusionsarrayOperations the kit considered and chose not to expose
registered_commands.all_private_chatsarrayCommand names registered for all private chats, up to 100 entries
registered_commands.all_group_chatsarrayCommand names registered for all group chats, up to 100 entries

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

NameInTypeRequiredDescription
forcequerystringNoRotate even though a poller is running. Without it an active poller refuses the rotation. Accepted values: true, false.
Error CodeTitleDescriptionResolution
keys_rotate_refusedKeys rotation refusedA poller is currently active; the kit will not rotate under a running poller unless force=true is passedStop the active poller, or call the endpoint again with ?force=true
keys_rotate_failedKeys rotation failedThe re-encryption or the new key commit did not complete; the database was rolled back to the previous keyInspect the kit logs, fix the underlying error, then retry
invalid_queryInvalid queryThe force parameter is not one of the allowed valuesPass force=true or force=false, or omit it entirely
query_ambiguousQuery ambiguousThe force parameter was supplied more than once with conflicting valuesSend force at most once per request
Terminal window
curl -X POST 'https://{projectId}-{containerId}-bot-1.{server}.containers.hoody.com/api/v1/bot/kit/keys/rotate?force=true'
{
"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
}
FieldTypeDescription
rotated_rowsintegerTotal rows touched across all sealed columns
generationintegerThe 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
columnsarrayOne entry per sealed column that was re-encrypted
columns[].tablestringThe table holding the sealed column
columns[].columnstringThe sealed column name
columns[].rowsintegerNumber of rows re-encrypted in that column
active_pollersintegerNumber of pollers running at the time of rotation
forcedbooleanTrue if force=true was passed and a poller was active