Containers
Section titled “Containers”The Containers API provides full lifecycle control of Hoody containers: list, retrieve, monitor, authorize, create, update, and delete containers, both globally across all projects and scoped to a single project. Use these endpoints to manage container settings (KVM, ramdisk scope, autostart, environment variables, SSH keys, realm membership), inspect live resource statistics and per-month proxied-request usage, and issue short-lived ED25519-signed container claims that programs inside the container can verify offline.
Listing containers
Section titled “Listing containers”GET /api/v1/containers/
Section titled “GET /api/v1/containers/”Get all containers across all projects for the current user with pagination, filtering, and sorting. This endpoint provides a global view of all containers without being scoped to a specific project. environment_vars is always {} in this response: read the variables with GET /api/v1/containers/{id}/env.
Parameters
Section titled “Parameters”| Name | In | Type | Required | Description |
|---|---|---|---|---|
page | query | number | No | Page number for pagination, starts from 1. Default: 1. |
limit | query | number | No | Number of containers to return per page, maximum 100 items. Default: 50. |
sort_by | query | string | No | Field to sort containers by. Allowed values: id, name, status, created_at, updated_at. Default: created_at. |
sort_order | query | string | No | Sort direction. Allowed values: asc, desc. Default: desc. |
realm_id | query | string | No | Filter by realm ID. Only returns containers that belong to this realm. Alternative to using a realm subdomain in the URL. |
runtime | query | string | No | Include live runtime information. Accepts true, false, or a URL-encoded JSON string like {"displays":true}. An empty JSON object {} fetches all info. Runtime information may be up to 2 seconds old. |
include_proxy_domains | query | boolean | No | Include proxy domains (aliases) for each container. When true, adds a proxy_domains array to each container object. |
include_proxy_permissions | query | boolean | No | Include the full proxy-permissions documents (container-level proxy_permissions and parent-project-level project_proxy_permissions) for each container. Returns proxy authentication group configuration including credentials, request only when explicitly needed. Auth tokens additionally require the resources.proxy_aliases permission. |
include_expired | query | boolean | No | Include containers that have expired due to server termination. By default, expired containers are excluded. |
include_deleting | query | boolean | No | Include containers currently being deleted. By default, deleting containers are excluded. |
Example
Section titled “Example”curl -X GET 'https://api.hoody.com/api/v1/containers/?page=1&limit=50' \ -H 'Authorization: Bearer <token>'import { HoodyClient } from 'hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://api.hoody.com', token: process.env.HOODY_TOKEN });
for await (const container of client.api.containers.listIterator({ limit: 50, sort_by: 'created_at' })) { console.log(container.id, container.name);}{ "statusCode": 200, "message": "Containers retrieved successfully", "data": { "containers": [ { "id": "890abcdef12345678901cdef", "project_id": "67e89abc123def456789abcd", "project_alias": "Production Environment", "server_id": "111abcdef12345678901aaaa", "server_name": "node-example-1", "subserver_name": "free-1", "server": { "name": "node-example-1", "country": "SG", "country_name": "Singapore", "city": "Singapore", "region": "Asia Pacific", "datacenter": "SIN-DC1", "is_free": true, "paid_subserver": false, "specs": { "cpu_cores": null, "ram_gb": null, "disk_gb": 50, "shared_compute": true }, "expires_at": null, "kvm_supported": false, "kvm_nested": false, "kvm_checked_at": null }, "ssh_hostname": "67e89abc123def456789abcd-890abcdef12345678901cdef-ssh.node-example-1.containers.hoody.com", "env_sync": { "pending": false, "revision": 4, "applied_revision": 4, "verified_at": "2025-01-15T14:25:00.000Z", "attempted_at": "2025-01-15T14:25:00.000Z", "last_error": null, "exhausted": false }, "name": "web-app-1", "color": "#3B82F6", "container_image": "debian/13", "ai": true, "hoody_kit": true, "dev_kit": true, "kvm": false, "autostart": true, "ramdisk_scope": "container", "ramdisk": true, "is_default": true, "status": "running", "environment_vars": {}, "volumes": {}, "ssh_public_key": "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC...", "ssh_key_fingerprint": "SHA256:wBF5vmw/82L9++9kd8PEmcEkErv5hBWXC7aJsuXAyh8", "ssh_key_installed_at": "2025-01-15T10:30:00.000Z", "comment": "Primary web application container", "source_container_id": null, "server_expired": false, "server_expired_at": null, "server_expired_reason": null, "created_at": "2025-01-15T10:30:00.000Z", "updated_at": "2025-01-15T10:30:00.000Z", "realm_ids": [], "snapshot_count": 3, "last_used_snapshot": "backup-2025-01-14", "runtime_info": null, "pool_id": null, "has_proxy_permissions": false, "proxy_permissions_scope": "none", "has_proxy_domains": false, "proxy_domains_count": 0 } ], "pagination": { "total": 5, "page": 1, "limit": 50, "totalPages": 1 } }}{ "statusCode": 400, "error": "Bad Request", "message": "Validation failed"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
VALIDATION_ERROR | Invalid input parameters | One or more request parameters failed validation. | Check the error message for specific field requirements and correct your input. |
INVALID_PARAMETER_VALUE | Invalid parameter value | A parameter value is outside the allowed range or format. | Ensure parameter values meet the documented constraints (min/max, format, regex). |
{ "statusCode": 401, "error": "Unauthorized", "message": "Authentication token required"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
MISSING_TOKEN | Authentication token missing | No authentication token was provided in the request. | Include a valid JWT token in the Authorization header as Bearer <token>. |
INVALID_TOKEN | Invalid authentication token | The provided authentication token is malformed or invalid. | Obtain a new token by logging in again or using a valid auth token. |
TOKEN_EXPIRED | Authentication token expired | The provided authentication token has expired. | Obtain a new token by logging in again or refreshing your session. |
AUTH_TOKEN_LINEAGE_REVOKED | Parent auth token no longer valid | The auth token was created by another auth token, and that token (or one above it) has been revoked, disabled or has expired, so this one no longer authenticates | Create a new auth token from a valid one, or sign in again |
EMAIL_NOT_VERIFIED | Email not verified | Returned by the login endpoint when the password is correct but the account’s email address has not been verified yet. It is only returned after the password has been checked. The response carries data.email so the client can offer a ‘resend verification email’ action without asking again. | Complete email verification by clicking the link sent to your inbox, or call /auth/resend-verification to receive a new link, or complete a password reset which also implicitly verifies the email. |
INVALID_OTP_CODE | Invalid OTP code | The provided 2FA code is incorrect or has expired | Generate a new code from your authenticator app and try again |
{ "statusCode": 403, "error": "Forbidden", "message": "Insufficient permissions"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
INSUFFICIENT_PERMISSIONS | Insufficient permissions | You do not have the required permissions to perform this action. | Ask the resource owner to grant you access. |
ACCOUNT_BANNED | Account banned | Your account has been banned and cannot access this resource. | Contact support for information about your account status. |
Validation Error: the request does not match the operation schema, or its JSON body cannot be parsed. message names each failing check, and data, when present, lists them one entry per failing field.
{ "statusCode": 422, "error": "Validation Error", "message": "Validation failed: /name must NOT have fewer than 3 characters"}GET /api/v1/projects/{id}/containers
Section titled “GET /api/v1/projects/{id}/containers”Get all containers for a single project with pagination, filtering, and sorting. environment_vars is always {} in this response: read the variables with GET /api/v1/containers/{id}/env.
Parameters
Section titled “Parameters”| Name | In | Type | Required | Description |
|---|---|---|---|---|
page | query | number | No | Page number for pagination, starts from 1. |
limit | query | number | No | Number of containers to return per page. Default: 50. |
sort_by | query | string | No | Field to sort containers by. Allowed values: id, name, status, created_at, updated_at. |
sort_order | query | string | No | Sort direction. Allowed values: asc, desc. |
runtime | query | string | No | Include live runtime information. Accepts true, false, or a URL-encoded JSON string like {"displays":true}. An empty JSON object {} fetches all info. Runtime information may be up to 2 seconds old. |
include_proxy_domains | query | boolean | No | Include proxy domains (aliases) for each container. |
include_proxy_permissions | query | boolean | No | Include the full proxy-permissions documents for each container. Auth tokens additionally require the resources.proxy_aliases permission. |
include_expired | query | boolean | No | Include containers that have expired due to server termination. |
include_deleting | query | boolean | No | Include containers currently being deleted. |
id | path | string | Yes | Unique identifier of the project. |
Example
Section titled “Example”curl -X GET 'https://api.hoody.com/api/v1/projects/67e89abc123def456789abcd/containers?page=1&limit=20' \ -H 'Authorization: Bearer <token>'import { HoodyClient } from 'hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://api.hoody.com', token: process.env.HOODY_TOKEN });
for await (const container of client.api.containers.listByProjectIterator('67e89abc123def456789abcd', { limit: 20 })) { console.log(container.id, container.name, container.status);}{ "statusCode": 200, "message": "Containers retrieved successfully", "data": { "containers": [ { "id": "890abcdef12345678901cdef", "project_id": "67e89abc123def456789abcd", "project_alias": "my-web-app", "server_id": "111abcdef12345678901aaaa", "server_name": "node-example-1", "subserver_name": "free-1", "server": { "name": "node-example-1", "country": "SG", "country_name": "Singapore", "city": "Singapore", "region": "Asia Pacific", "datacenter": "SIN-DC1", "is_free": true, "paid_subserver": false, "specs": { "cpu_cores": null, "ram_gb": null, "disk_gb": 50, "shared_compute": true }, "expires_at": null, "kvm_supported": false, "kvm_nested": false, "kvm_checked_at": null }, "ssh_hostname": "67e89abc123def456789abcd-890abcdef12345678901cdef-ssh.node-example-1.containers.hoody.com", "env_sync": { "pending": false, "revision": 2, "applied_revision": 2, "verified_at": "2025-01-15T11:10:00.000Z", "attempted_at": "2025-01-15T11:10:00.000Z", "last_error": null, "exhausted": false }, "name": "frontend-prod", "color": "#3B82F6", "container_image": "debian/13", "ai": true, "hoody_kit": true, "dev_kit": true, "kvm": false, "autostart": true, "is_default": false, "status": "running", "environment_vars": {}, "volumes": {}, "ssh_public_key": "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOMqqnkVzrm0SdG6UOoqKLsabgH5C9okWi0dh2l9GKJl user@host", "ssh_key_fingerprint": "SHA256:wBF5vmw/82L9++9kd8PEmcEkErv5hBWXC7aJsuXAyh8", "ssh_key_installed_at": "2025-01-15T10:30:00.000Z", "comment": "Production frontend server", "created_at": "2025-01-15T10:30:00.000Z", "updated_at": "2025-01-15T14:22:00.000Z", "realm_ids": [], "snapshot_count": 3, "last_used_snapshot": "before-upgrade", "runtime_info": null, "pool_id": null, "has_proxy_permissions": false, "proxy_permissions_scope": "none", "has_proxy_domains": false, "proxy_domains_count": 0 } ], "pagination": { "total": 42, "page": 1, "limit": 20, "totalPages": 3 } }}{ "statusCode": 400, "error": "Bad Request", "message": "Invalid ID format"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
VALIDATION_ERROR | Invalid input parameters | One or more request parameters failed validation. | Check the error message for specific field requirements and correct your input. |
INVALID_ID_FORMAT | Invalid ID format | The provided ID must be a 24-character hexadecimal string. | Ensure the ID is exactly 24 characters long and contains only hexadecimal characters (0-9, a-f). |
{ "statusCode": 401, "error": "Unauthorized", "message": "Authentication token required"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
MISSING_TOKEN | Authentication token missing | No authentication token was provided in the request. | Include a valid JWT token in the Authorization header as Bearer <token>. |
INVALID_TOKEN | Invalid authentication token | The provided authentication token is malformed or invalid. | Obtain a new token by logging in again or using a valid auth token. |
TOKEN_EXPIRED | Authentication token expired | The provided authentication token has expired. | Obtain a new token by logging in again or refreshing your session. |
AUTH_TOKEN_LINEAGE_REVOKED | Parent auth token no longer valid | The auth token was created by another auth token, and that token (or one above it) has been revoked, disabled or has expired, so this one no longer authenticates | Create a new auth token from a valid one, or sign in again |
EMAIL_NOT_VERIFIED | Email not verified | Returned by the login endpoint when the password is correct but the account’s email address has not been verified yet. It is only returned after the password has been checked. The response carries data.email so the client can offer a ‘resend verification email’ action without asking again. | Complete email verification by clicking the link sent to your inbox, or call /auth/resend-verification to receive a new link, or complete a password reset which also implicitly verifies the email. |
INVALID_OTP_CODE | Invalid OTP code | The provided 2FA code is incorrect or has expired | Generate a new code from your authenticator app and try again |
{ "statusCode": 403, "error": "Forbidden", "message": "Insufficient permissions"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
INSUFFICIENT_PERMISSIONS | Insufficient permissions | You do not have the required permissions to perform this action. | Ask the resource owner to grant you access. |
ACCOUNT_BANNED | Account banned | Your account has been banned and cannot access this resource. | Contact support for information about your account status. |
{ "statusCode": 404, "error": "Not Found", "message": "Resource not found"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
RESOURCE_NOT_FOUND | Resource not found | The requested resource does not exist or has been deleted. | Verify the resource ID and ensure it exists. |
Validation Error: the request does not match the operation schema, or its JSON body cannot be parsed. message names each failing check, and data, when present, lists them one entry per failing field.
{ "statusCode": 422, "error": "Validation Error", "message": "Validation failed: /name must NOT have fewer than 3 characters"}Retrieving a container
Section titled “Retrieving a container”GET /api/v1/containers/{id}
Section titled “GET /api/v1/containers/{id}”Retrieve a single container by ID, with optional live runtime information, proxy domains, and full proxy-permissions documents.
Parameters
Section titled “Parameters”| Name | In | Type | Required | Description |
|---|---|---|---|---|
id | path | string | Yes | Unique identifier of the container to retrieve. |
runtime | query | string | No | Include live runtime information. Accepts true, false, or a URL-encoded JSON string like {"displays":true}. An empty JSON object {} fetches all info. Runtime information may be up to 2 seconds old. |
include_proxy_domains | query | boolean | No | Include proxy domains (aliases) for this container. |
include_proxy_permissions | query | boolean | No | Include the full proxy-permissions documents. Auth tokens additionally require the resources.proxy_aliases permission. |
Example
Section titled “Example”curl -X GET 'https://api.hoody.com/api/v1/containers/890abcdef12345678901cdef' \ -H 'Authorization: Bearer <token>'import { HoodyClient } from 'hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://api.hoody.com', token: process.env.HOODY_TOKEN });
const container = await client.api.containers.get('890abcdef12345678901cdef', { runtime: 'true' });console.log(container.data.name, container.data.status);{ "statusCode": 200, "message": "Container retrieved successfully", "data": { "id": "890abcdef12345678901cdef", "project_id": "67e89abc123def456789abcd", "project_alias": "Production Environment", "server_id": "111abcdef12345678901aaaa", "server_name": "node-example-1", "subserver_name": "free-1", "server": { "name": "node-example-1", "country": "SG", "country_name": "Singapore", "city": "Singapore", "region": "Asia Pacific", "datacenter": "SIN-DC1", "is_free": true, "paid_subserver": false, "specs": { "cpu_cores": null, "ram_gb": null, "disk_gb": 50, "shared_compute": true }, "expires_at": null, "kvm_supported": false, "kvm_nested": false, "kvm_checked_at": null }, "ssh_hostname": "67e89abc123def456789abcd-890abcdef12345678901cdef-ssh.node-example-1.containers.hoody.com", "env_sync": { "pending": false, "revision": 4, "applied_revision": 4, "verified_at": "2025-01-15T14:25:00.000Z", "attempted_at": "2025-01-15T14:25:00.000Z", "last_error": null, "exhausted": false }, "name": "web-app-1", "color": "#3B82F6", "container_image": "debian/13", "ai": true, "hoody_kit": true, "dev_kit": true, "kvm": false, "autostart": true, "ramdisk_scope": "container", "ramdisk": true, "is_default": true, "status": "running", "environment_vars": { "NODE_ENV": "production" }, "volumes": {}, "ssh_public_key": "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC...", "ssh_key_fingerprint": "SHA256:wBF5vmw/82L9++9kd8PEmcEkErv5hBWXC7aJsuXAyh8", "ssh_key_installed_at": "2025-01-15T10:30:00.000Z", "comment": "Primary web application container", "source_container_id": null, "server_expired": false, "server_expired_at": null, "server_expired_reason": null, "created_at": "2025-01-15T10:30:00.000Z", "updated_at": "2025-01-15T10:30:00.000Z", "realm_ids": [], "snapshot_count": 3, "last_used_snapshot": "backup-2025-01-14", "warnings": [], "runtime_info": null, "pool_id": null, "has_proxy_permissions": false, "proxy_permissions_scope": "none", "has_proxy_domains": false, "proxy_domains_count": 0 }}{ "statusCode": 400, "error": "Bad Request", "message": "Invalid ID format"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
INVALID_ID_FORMAT | Invalid ID format | The provided ID must be a 24-character hexadecimal string. | Ensure the ID is exactly 24 characters long and contains only hexadecimal characters (0-9, a-f). |
{ "statusCode": 401, "error": "Unauthorized", "message": "Authentication token required"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
MISSING_TOKEN | Authentication token missing | No authentication token was provided in the request. | Include a valid JWT token in the Authorization header as Bearer <token>. |
INVALID_TOKEN | Invalid authentication token | The provided authentication token is malformed or invalid. | Obtain a new token by logging in again or using a valid auth token. |
TOKEN_EXPIRED | Authentication token expired | The provided authentication token has expired. | Obtain a new token by logging in again or refreshing your session. |
AUTH_TOKEN_LINEAGE_REVOKED | Parent auth token no longer valid | The auth token was created by another auth token, and that token (or one above it) has been revoked, disabled or has expired, so this one no longer authenticates | Create a new auth token from a valid one, or sign in again |
EMAIL_NOT_VERIFIED | Email not verified | Returned by the login endpoint when the password is correct but the account’s email address has not been verified yet. It is only returned after the password has been checked. The response carries data.email so the client can offer a ‘resend verification email’ action without asking again. | Complete email verification by clicking the link sent to your inbox, or call /auth/resend-verification to receive a new link, or complete a password reset which also implicitly verifies the email. |
INVALID_OTP_CODE | Invalid OTP code | The provided 2FA code is incorrect or has expired | Generate a new code from your authenticator app and try again |
{ "statusCode": 403, "error": "Forbidden", "message": "Insufficient permissions"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
INSUFFICIENT_PERMISSIONS | Insufficient permissions | You do not have the required permissions to perform this action. | Ask the resource owner to grant you access. |
ACCOUNT_BANNED | Account banned | Your account has been banned and cannot access this resource. | Contact support for information about your account status. |
{ "statusCode": 404, "error": "Not Found", "message": "Container not found"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
CONTAINER_NOT_FOUND | Container not found | The requested container does not exist or you do not have permission to access it. | Verify the container ID is correct and that you have access to the project it belongs to. |
Validation Error: the request does not match the operation schema, or its JSON body cannot be parsed. message names each failing check, and data, when present, lists them one entry per failing field.
{ "statusCode": 422, "error": "Validation Error", "message": "Validation failed: /name must NOT have fewer than 3 characters"}Monitoring containers
Section titled “Monitoring containers”GET /api/v1/containers/{id}/proxy-usage
Section titled “GET /api/v1/containers/{id}/proxy-usage”Per-month proxied request counts and response bytes for one container.
Counts completed, routed, non-excluded HTTP responses only. They do not include forward-proxy/CONNECT egress, direct SSH, raw container egress, or WebSocket sessions until they close. Live containers only are reported; deleted containers are excluded. The totals field sums the documents actually returned. It is NOT comprehensive whenever coverage_complete is false (some usage could not be read, or a month is no longer available), and totals always excludes deleted containers. totals.exact is false if a sum exceeded the exact integer range. Older months are kept only if they were read before they expired, so a month never queried in time may be gone permanently. A month withdrawn by an ownership change stops being served.
Parameters
Section titled “Parameters”| Name | In | Type | Required | Description |
|---|---|---|---|---|
from | query | string | Yes | First month, inclusive (YYYY-MM). |
to | query | string | Yes | Last month, inclusive (YYYY-MM). Maximum 12 months. |
id | path | string | Yes | Container ID. |
Example
Section titled “Example”curl -X GET 'https://api.hoody.com/api/v1/containers/67e89abc123def456789abcd/proxy-usage?from=2025-01&to=2025-03' \ -H 'Authorization: Bearer <token>'import { HoodyClient } from 'hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://api.hoody.com', token: process.env.HOODY_TOKEN });
const usage = await client.api.containers.getProxyUsage('67e89abc123def456789abcd', { from: '2025-01', to: '2025-03'});console.log(usage.data.totals);{ "statusCode": 200, "message": "Proxy usage retrieved successfully", "data": { "from": "2025-01", "to": "2025-03", "excludes_deleted_containers": true, "retrieval_complete": true, "coverage_complete": true, "months_capped": false, "totals": { "requests": 124053, "response_bytes": 4823092010, "exact": true }, "retention": { "enforced": true, "retained_from": "2024-12-01" }, "sources": [ { "status": "ok", "reader_stale": false } ], "containers": { "67e89abc123def456789abcd": { "2025-01": { "month": "2025-01", "updated": "2025-02-01T00:00:00.000Z", "days": { "2025-01-15": { "requests": 4123, "response_bytes": 182341120, "slots": 86400, "active_slots": 4123, "kits": { "hoody_kit": 4123 }, "slot_ms": 184230000 } }, "totals": { "requests": 4123, "response_bytes": 182341120, "exact": true }, "measurement": { "excluded_kits": [] } } } } }}{ "statusCode": 400, "error": "Bad Request", "message": "Invalid parameter value"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
INVALID_ID_FORMAT | Invalid ID format | The provided ID must be a 24-character hexadecimal string. | Ensure the ID is exactly 24 characters long and contains only hexadecimal characters (0-9, a-f). |
INVALID_PARAMETER_VALUE | Invalid parameter value | A parameter value is outside the allowed range or format. | Ensure parameter values meet the documented constraints (min/max, format, regex). |
{ "statusCode": 401, "error": "Unauthorized", "message": "Authentication token required"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
MISSING_TOKEN | Authentication token missing | No authentication token was provided in the request. | Include a valid JWT token in the Authorization header as Bearer <token>. |
INVALID_TOKEN | Invalid authentication token | The provided authentication token is malformed or invalid. | Obtain a new token by logging in again or using a valid auth token. |
TOKEN_EXPIRED | Authentication token expired | The provided authentication token has expired. | Obtain a new token by logging in again or refreshing your session. |
AUTH_TOKEN_LINEAGE_REVOKED | Parent auth token no longer valid | The auth token was created by another auth token, and that token (or one above it) has been revoked, disabled or has expired, so this one no longer authenticates | Create a new auth token from a valid one, or sign in again |
EMAIL_NOT_VERIFIED | Email not verified | Returned by the login endpoint when the password is correct but the account’s email address has not been verified yet. It is only returned after the password has been checked. The response carries data.email so the client can offer a ‘resend verification email’ action without asking again. | Complete email verification by clicking the link sent to your inbox, or call /auth/resend-verification to receive a new link, or complete a password reset which also implicitly verifies the email. |
INVALID_OTP_CODE | Invalid OTP code | The provided 2FA code is incorrect or has expired | Generate a new code from your authenticator app and try again |
{ "statusCode": 403, "error": "Forbidden", "message": "Access denied"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
INSUFFICIENT_PERMISSIONS | Insufficient permissions | You do not have the required permissions to perform this action. | Ask the resource owner to grant you access. |
RESOURCE_ACCESS_DENIED | Resource access denied | You do not have permission to access this specific resource. | Ensure you own this resource or have been granted access by the owner. |
{ "statusCode": 404, "error": "Not Found", "message": "Resource not found"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
RESOURCE_NOT_FOUND | Resource not found | The requested resource does not exist or has been deleted. | Verify the resource ID and ensure it exists. |
Validation Error: the request does not match the operation schema, or its JSON body cannot be parsed. message names each failing check, and data, when present, lists them one entry per failing field.
{ "statusCode": 422, "error": "Validation Error", "message": "Validation failed: /name must NOT have fewer than 3 characters"}{ "statusCode": 429, "error": "Too Many Requests", "message": "Rate limit exceeded"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
RATE_LIMIT_EXCEEDED | Rate limit exceeded | You have exceeded the rate limit for this endpoint. | Wait before making additional requests, or upgrade your plan for higher limits. |
{ "statusCode": 500, "error": "Internal Server Error", "message": "An unexpected error occurred"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
INTERNAL_SERVER_ERROR | Internal server error | An unexpected error occurred on the server. | Try again later, or contact support if the problem persists. |
{ "statusCode": 502, "message": "Partial proxy usage response", "data": { "from": "2025-01", "to": "2025-03", "excludes_deleted_containers": true, "retrieval_complete": false, "coverage_complete": false, "months_capped": false, "totals": { "requests": 0, "response_bytes": 0, "exact": true }, "retention": { "enforced": true, "retained_from": null }, "sources": [ { "status": "source_error", "reader_stale": false } ], "containers": {} }}GET /api/v1/containers/{id}/stats
Section titled “GET /api/v1/containers/{id}/stats”Get real-time resource usage statistics for a container, including CPU, memory, disk, network, and ramdisk metrics.
data.started_at is when the container was started, or null when that is not known.
Parameters
Section titled “Parameters”| Name | In | Type | Required | Description |
|---|---|---|---|---|
id | path | string | Yes | Unique identifier of the container. |
Example
Section titled “Example”curl -X GET 'https://api.hoody.com/api/v1/containers/67e89abc123def456789abcd/stats' \ -H 'Authorization: Bearer <token>'import { HoodyClient } from 'hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://api.hoody.com', token: process.env.HOODY_TOKEN });
const stats = await client.api.containers.getStats('67e89abc123def456789abcd');console.log(stats.data.cpu.usage_percent, stats.data.memory.usage_percent);{ "statusCode": 200, "message": "Container statistics retrieved successfully", "data": { "id": "67e89abc123def456789abcd", "project_id": "890abcdef12345678901cdef", "project_name": "Production Environment", "server_name": "node-example-1", "subserver_name": "free-1", "status": "Running", "status_code": 103, "processes": 143, "started_at": "2025-12-03T18:39:53.938688987Z", "cpu": { "usage": 17303605000, "allocated_time": 24000000000, "usage_percent": 72.1 }, "memory": { "usage": 1474666496, "total": 131503332000, "usage_percent": 1.12, "swap_usage": 0, "swap_usage_peak": 0, "usage_peak": 0 }, "ramdisk": { "scope": "container", "shared_pool_maximum": 536870912, "capacity_reserved": false, "usage": { "private": 4096, "secrets": 0 } }, "disk": { "root": { "total": 0, "usage": 11081670656 } }, "network": [ { "interface": "eth0", "addresses": [ { "address": "10.10.0.122", "family": "inet", "netmask": "30", "scope": "global" } ], "counters": { "bytes_received": 1098069936, "bytes_sent": 16777319, "packets_received": 346440, "packets_sent": 242010, "errors_received": 0, "errors_sent": 0, "packets_dropped_inbound": 0, "packets_dropped_outbound": 0 }, "state": "up", "type": "broadcast" } ], "processing_time": "3ms" }}{ "statusCode": 400, "error": "Bad Request", "message": "Invalid ID format"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
INVALID_ID_FORMAT | Invalid ID format | The provided ID must be a 24-character hexadecimal string. | Ensure the ID is exactly 24 characters long and contains only hexadecimal characters (0-9, a-f). |
{ "statusCode": 401, "error": "Unauthorized", "message": "Authentication token required"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
MISSING_TOKEN | Authentication token missing | No authentication token was provided in the request. | Include a valid JWT token in the Authorization header as Bearer <token>. |
INVALID_TOKEN | Invalid authentication token | The provided authentication token is malformed or invalid. | Obtain a new token by logging in again or using a valid auth token. |
TOKEN_EXPIRED | Authentication token expired | The provided authentication token has expired. | Obtain a new token by logging in again or refreshing your session. |
AUTH_TOKEN_LINEAGE_REVOKED | Parent auth token no longer valid | The auth token was created by another auth token, and that token (or one above it) has been revoked, disabled or has expired, so this one no longer authenticates | Create a new auth token from a valid one, or sign in again |
EMAIL_NOT_VERIFIED | Email not verified | Returned by the login endpoint when the password is correct but the account’s email address has not been verified yet. It is only returned after the password has been checked. The response carries data.email so the client can offer a ‘resend verification email’ action without asking again. | Complete email verification by clicking the link sent to your inbox, or call /auth/resend-verification to receive a new link, or complete a password reset which also implicitly verifies the email. |
INVALID_OTP_CODE | Invalid OTP code | The provided 2FA code is incorrect or has expired | Generate a new code from your authenticator app and try again |
{ "statusCode": 403, "error": "Forbidden", "message": "Insufficient permissions"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
INSUFFICIENT_PERMISSIONS | Insufficient permissions | You do not have the required permissions to perform this action. | Ask the resource owner to grant you access. |
ACCOUNT_BANNED | Account banned | Your account has been banned and cannot access this resource. | Contact support for information about your account status. |
RESOURCE_ACCESS_DENIED | Resource access denied | You do not have permission to access this specific resource. | Ensure you own this resource or have been granted access by the owner. |
{ "statusCode": 404, "error": "Not Found", "message": "Container not found"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
CONTAINER_NOT_FOUND | Container not found | The requested container does not exist or you do not have permission to access it. | Verify the container ID is correct and that you have access to the project it belongs to. |
RESOURCE_NOT_FOUND | Resource not found | The requested resource does not exist or has been deleted. | Verify the resource ID and ensure it exists. |
Validation Error: the request does not match the operation schema, or its JSON body cannot be parsed. message names each failing check, and data, when present, lists them one entry per failing field.
{ "statusCode": 422, "error": "Validation Error", "message": "Validation failed: /name must NOT have fewer than 3 characters"}{ "statusCode": 500, "error": "Internal Server Error", "message": "An unexpected error occurred"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
INTERNAL_SERVER_ERROR | Internal server error | An unexpected error occurred on the server. | Try again later, or contact support if the problem persists. |
EXTERNAL_SERVICE_ERROR | External service error | A required external service is unavailable or returned an error. | Try again later when the external service is available. |
Authorizing container access
Section titled “Authorizing container access”POST /api/v1/containers/{id}/authorize
Section titled “POST /api/v1/containers/{id}/authorize”Issue a signed, portable container authorization claim. The returned container_claim is an ED25519-signed credential that proves who the user is (sub, type), which container they are authorized for (containerId, projectId), and when Hoody issued the authorization (iat, exp).
Container programs can verify this claim offline using Hoody’s public key at GET /api/v1/meta/public-key; no API round-trip is needed during verification.
Claims expire after a few hours (6h by default). expires_in gives the exact lifetime in seconds. Clients should re-call this endpoint to refresh before expiry.
Parameters
Section titled “Parameters”| Name | In | Type | Required | Description |
|---|---|---|---|---|
id | path | string | Yes | Container ID (24-character hexadecimal string). |
Example
Section titled “Example”curl -X POST 'https://api.hoody.com/api/v1/containers/67e89abc123def456789abcd/authorize' \ -H 'Authorization: Bearer <token>'import { HoodyClient } from 'hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://api.hoody.com', token: process.env.HOODY_TOKEN });
const claim = await client.api.containers.createClaim('67e89abc123def456789abcd');console.log(claim.data.container_claim.kid, claim.data.expires_in);{ "statusCode": 200, "message": "Container authorization claim issued", "data": { "container_claim": { "kid": "v1", "payload_b64": "eyJjbGFpbV90eXBlIjoiY29udGFpbmVyIiwiY29udGFpbmVySWQiOiI2N2U4OWFiYzEyM2RlZjQ1Njc4OWFiY2QiLCJwcm9qZWN0SWQiOiI4OTBhYmNkZWYxMjM0NTY3ODkwMWNkZWYiLCJzdWIiOiJ1c2VyXzEyMyIsImlhdCI6MTczNzEwNjkzMDAsImV4cCI6MTczNzEyNDYzMTl9", "signature_hex": "5d3f8b5e5d3f8b5e5d3f8b5e5d3f8b5e5d3f8b5e5d3f8b5e5d3f8b5e5d3f8b5e5d3f8b5e5d3f8b5e5d3f8b5e5d3f8b5e5d3f8b5e5d3f8b5e5d3f8b5e5d3f8b5e" }, "expires_in": 21600, "container_id": "67e89abc123def456789abcd", "project_id": "890abcdef12345678901cdef" }}The successful response is also signed by the server. Inspect the response header X-Hoody-Signature (format: t=<unix_ts>,kid=<keyId>,path=<urlPath>,sig=<128-hex>) to authenticate that Hoody produced the claim and that it has not been tampered with in transit.
{ "statusCode": 400, "error": "Bad Request", "message": "Invalid ID format"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
INVALID_ID_FORMAT | Invalid ID format | The provided ID must be a 24-character hexadecimal string. | Ensure the ID is exactly 24 characters long and contains only hexadecimal characters (0-9, a-f). |
{ "statusCode": 401, "error": "Unauthorized", "message": "Authentication token required"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
MISSING_TOKEN | Authentication token missing | No authentication token was provided in the request. | Include a valid JWT token in the Authorization header as Bearer <token>. |
{ "statusCode": 403, "error": "Forbidden", "message": "Insufficient permissions"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
INSUFFICIENT_PERMISSIONS | Insufficient permissions | You do not have the required permissions to perform this action. | Ask the resource owner to grant you access. |
{ "statusCode": 404, "error": "Not Found", "message": "Container not found"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
CONTAINER_NOT_FOUND | Container not found | The requested container does not exist or you do not have permission to access it. | Verify the container ID is correct and that you have access to the project it belongs to. |
Validation Error: the request does not match the operation schema, or its JSON body cannot be parsed. message names each failing check, and data, when present, lists them one entry per failing field.
{ "statusCode": 422, "error": "Validation Error", "message": "Validation failed: /name must NOT have fewer than 3 characters"}{ "statusCode": 503, "error": "SIGNING_NOT_CONFIGURED", "message": "Response signing is not available"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
OPERATION_STATE_CONFLICT | Container State Conflict | The operation cannot be performed because the container is not in the correct state. | Check the container’s current status. For example, a container must be stopped to be started. |
Creating a container
Section titled “Creating a container”POST /api/v1/projects/{id}/containers
Section titled “POST /api/v1/projects/{id}/containers”Create a new container inside a project. The container starts in the creating state and progresses to running once provisioning finishes on the chosen server. environment_vars is always {} in this response: read the variables with GET /api/v1/containers/{id}/env.
Parameters
Section titled “Parameters”| Name | In | Type | Required | Description |
|---|---|---|---|---|
id | path | string | Yes | Unique identifier of the project that will own the new container. |
Request Body
Section titled “Request Body”| Name | Type | Required | Description |
|---|---|---|---|
server_id | string | Yes | Unique identifier of the server that will run the container. |
name | string | No | Human-readable name, 3 to 100 characters, alphanumeric with hyphens and underscores. Omit or use "rand" to generate a random name. |
color | string | No | Hex color for the container (e.g., #FF0000 or FF0000). A # prefix is added automatically if missing, and the color is converted to uppercase. |
container_image | string | No | Container image to use. Bare distribution names and hyphenated versions (debian, debian-13) are resolved to the canonical base image (debian/13), as are debian:13 and debian 13. If null or omitted, the default configured image is used. |
ai | boolean | No | Whether AI features are enabled. Defaults to true for JWT callers; for auth-token callers, defaults to whether the token has containers.features.ai permission. |
environment_vars | object | No | Object of environment variables to set in the container. Keys must match ^[a-zA-Z_][a-zA-Z0-9_]*$ and be at most 128 characters long. Maximum 256 keys; each value 1 to 4096 characters. |
ssh_public_key | string | No | SSH public key for container access. SSH public keys must be unique per container (one container per key). If omitted, the key is inherited from project defaults. |
comment | string | No | Optional comment for the container, max 16000 characters. |
hoody_kit | boolean | No | Enable all Hoody Kit features: extra package sources, a package download cache, basic and developer packages, extra shells, the Hoody daemon, sudo setup, snapd removal, web view, desktop, a default user, a web terminal, and VM tooling (about 185 MiB; running VMs additionally needs the separate kvm grant). Defaults to true for JWT callers; for auth-token callers, defaults to whether the token has containers.features.hoody_kit permission. |
dev_kit | boolean | No | Enable dev_kit development tools in the container. Defaults to true when hoody_kit is true, false otherwise. Cannot be updated after creation. |
kvm | boolean | No | Enable /dev/kvm passthrough (run full VMs inside the container) at creation. Available on rented or dedicated (bare-metal) servers only, never free tier; rejected with 403 on a free server. Default: false. Can also be toggled later via PUT /api/v1/containers/{id}/kvm on a stopped container. |
dev_kvm | boolean | No | Accepted alias of kvm on input. If both are sent, kvm wins and the two must agree. |
autostart | boolean | No | Whether the container starts automatically on host boot. Default: true. |
ramdisk | boolean | No | Whether to mount a ramdisk at /ramdisk. Keeps data on container stop/start/reboot but LOSES data on a host reboot. Up to 50% of total host memory. Default: true. |
cache | boolean | No | Enable use of cached images during container creation. When false, the container is built without the image cache. Default: true. |
cache_image | boolean | No | Force creation of a new cached image from the container image. Only available to the image owner. Default: false. |
realm_ids | array | No | Realm IDs to assign this container to. If creating from a realm subdomain (e.g., https://realm-abc.api.hoody.com), the subdomain realm is automatically included and merged with any explicitly provided realm_ids. Containers can have different realm membership than their parent project. |
Example
Section titled “Example”curl -X POST 'https://api.hoody.com/api/v1/projects/67e89abc123def456789abcd/containers' \ -H 'Authorization: Bearer <token>' \ -H 'Content-Type: application/json' \ -d '{ "server_id": "111abcdef12345678901aaaa", "name": "backend-api", "container_image": "debian/13", "ai": true, "environment_vars": { "DATABASE_URL": "postgresql://user:pass@db:5432/app", "REDIS_URL": "redis://cache:6379" }, "ssh_public_key": "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOMqqnkVzrm0SdG6UOoqKLsabgH5C9okWi0dh2l9GKJl user@host", "comment": "Backend API server with PostgreSQL connection", "autostart": true, "ramdisk": true }'import { HoodyClient } from 'hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://api.hoody.com', token: process.env.HOODY_TOKEN });
const container = await client.api.containers.create('67e89abc123def456789abcd', { server_id: '111abcdef12345678901aaaa', name: 'backend-api', container_image: 'debian/13', environment_vars: { DATABASE_URL: 'postgresql://user:pass@db:5432/app', REDIS_URL: 'redis://cache:6379' }, ssh_public_key: 'ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOMqqnkVzrm0SdG6UOoqKLsabgH5C9okWi0dh2l9GKJl user@host', comment: 'Backend API server with PostgreSQL connection'});console.log(container.data.id, container.data.status);{ "statusCode": 201, "message": "Container created successfully", "data": { "id": "67e89abc123def456789abce", "project_id": "67e89abc123def456789abcd", "project_alias": "my-web-app", "server_id": "111abcdef12345678901aaaa", "server_name": "node-example-1", "subserver_name": "free-1", "ssh_hostname": "67e89abc123def456789abcd-67e89abc123def456789abce-ssh.node-example-1.containers.hoody.com", "env_sync": { "pending": true, "revision": 1, "applied_revision": 0, "verified_at": null, "attempted_at": null, "last_error": null, "exhausted": false }, "name": "backend-api", "color": "#10B981", "container_image": "debian/13", "ai": true, "hoody_kit": true, "dev_kit": true, "kvm": false, "autostart": true, "status": "creating", "environment_vars": {}, "volumes": {}, "ssh_public_key": "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOMqqnkVzrm0SdG6UOoqKLsabgH5C9okWi0dh2l9GKJl user@host", "ssh_key_fingerprint": "SHA256:wBF5vmw/82L9++9kd8PEmcEkErv5hBWXC7aJsuXAyh8", "ssh_key_installed_at": null, "comment": "Backend API server with PostgreSQL connection", "created_at": "2025-01-15T15:45:00.000Z", "updated_at": "2025-01-15T15:45:00.000Z", "realm_ids": [] }}{ "statusCode": 400, "error": "Bad Request", "message": "Validation failed"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
VALIDATION_ERROR | Invalid input parameters | One or more request parameters failed validation. | Check the error message for specific field requirements and correct your input. |
INVALID_ID_FORMAT | Invalid ID format | The provided ID must be a 24-character hexadecimal string. | Ensure the ID is exactly 24 characters long and contains only hexadecimal characters (0-9, a-f). |
INVALID_CONTAINER_NAME | Invalid container name | Container name must be 3 to 100 characters, alphanumeric with hyphens and underscores. | Use a valid name between 3 and 100 characters containing only a-z, A-Z, 0-9, -, and _. |
SERVER_CONTAINER_LIMIT | Server container limit reached | The target server is at its maximum number of live containers (explicit max_containers, or the free-tier default). | Delete an existing container on this server, or create the container on a different server. |
PROJECT_CONTAINER_LIMIT | Project container limit reached | The target project is at its owner-configured container quota (the project’s max_containers setting). | Delete an existing container in this project, raise the project quota, or use a different project. |
{ "statusCode": 401, "error": "Unauthorized", "message": "Authentication token required"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
MISSING_TOKEN | Authentication token missing | No authentication token was provided in the request. | Include a valid JWT token in the Authorization header as Bearer <token>. |
INVALID_TOKEN | Invalid authentication token | The provided authentication token is malformed or invalid. | Obtain a new token by logging in again or using a valid auth token. |
TOKEN_EXPIRED | Authentication token expired | The provided authentication token has expired. | Obtain a new token by logging in again or refreshing your session. |
AUTH_TOKEN_LINEAGE_REVOKED | Parent auth token no longer valid | The auth token was created by another auth token, and that token (or one above it) has been revoked, disabled or has expired, so this one no longer authenticates | Create a new auth token from a valid one, or sign in again |
EMAIL_NOT_VERIFIED | Email not verified | Returned by the login endpoint when the password is correct but the account’s email address has not been verified yet. It is only returned after the password has been checked. The response carries data.email so the client can offer a ‘resend verification email’ action without asking again. | Complete email verification by clicking the link sent to your inbox, or call /auth/resend-verification to receive a new link, or complete a password reset which also implicitly verifies the email. |
INVALID_OTP_CODE | Invalid OTP code | The provided 2FA code is incorrect or has expired | Generate a new code from your authenticator app and try again |
{ "statusCode": 403, "error": "Forbidden", "message": "Insufficient permissions"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
INSUFFICIENT_PERMISSIONS | Insufficient permissions | You do not have the required permissions to perform this action. | Ask the resource owner to grant you access. |
ACCOUNT_BANNED | Account banned | Your account has been banned and cannot access this resource. | Contact support for information about your account status. |
{ "statusCode": 404, "error": "Not Found", "message": "Resource not found"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
RESOURCE_NOT_FOUND | Resource not found | The requested resource does not exist or has been deleted. | Verify the resource ID and ensure it exists. |
{ "statusCode": 409, "error": "Conflict", "message": "Container name already in use within the project"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
CONTAINER_NAME_IN_USE | Container name already in use | A container with this name already exists in the project. | Choose a different name for your container. |
SSH_PUBLIC_KEY_IN_USE | SSH public key already in use | SSH public keys must be unique per container. A single public key cannot be assigned to multiple containers. | Generate a new SSH key pair for this container, or remove the key from the other container before reusing it. |
{ "statusCode": 422, "error": "Unprocessable Entity", "message": "Quota exceeded"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
QUOTA_EXCEEDED | Quota exceeded | You have exceeded your quota for this resource type. | Delete unused resources or upgrade your plan for higher limits. |
{ "statusCode": 500, "error": "CONTAINER_CREATE_DISPATCH_FAILED", "message": "Container creation could not be started on its host"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
CONTAINER_CREATE_DISPATCH_FAILED | Container creation could not be started | The create request could not be sent to the server, so nothing was created. Its name, SSH key, and quota slot are free again. | Retry the request. If it keeps failing, try another server or contact support. |
| Error Code | Title | Description | Resolution |
|---|---|---|---|
CONTAINER_HOST_UNAVAILABLE | Container server temporarily unavailable | The server for this container could not be reached for a moment, so the create or copy request was never sent and nothing was created. Its name, SSH key and quota slot are free again. | Retry after the number of seconds in the Retry-After header. |
Updating containers
Section titled “Updating containers”PUT /api/v1/containers/{id}
Section titled “PUT /api/v1/containers/{id}”Also accepted: PATCH (same endpoint, identical behaviour).
Update an existing container’s editable settings, including environment variables, autostart, ramdisk configuration, SSH key, comment, and realm membership. Omitted fields are preserved.
Parameters
Section titled “Parameters”| Name | In | Type | Required | Description |
|---|---|---|---|---|
id | path | string | Yes | Unique identifier of the container to update. |
Request Body
Section titled “Request Body”| Name | Type | Required | Description |
|---|---|---|---|
name | string | No | Human-readable name for the container, at most 100 characters. Must be unique within the project; a name the server refuses answers INVALID_CONTAINER_NAME. |
color | string | No | Hex color for the container (e.g., #FF0000 or FF0000). A # prefix is added automatically if missing, and the color is converted to uppercase. |
ai | boolean | No | Whether AI features are enabled. If omitted, the current value is preserved. |
autostart | boolean | No | Whether the container starts automatically on host boot. If omitted, the current value is preserved. |
ramdisk_scope | string | No | Sharing scope for /ramdisk. Allowed values: container, project. container (default) mounts only private storage. project additionally mounts /ramdisk/project, shared with other containers in this project on the same server (a RAM disk cannot span servers). Refused when the project has other members. |
ramdisk | boolean | No | Whether to mount a ramdisk at /ramdisk. If omitted, the current value is preserved. Counts against memory rather than disk. Data survives container restarts but is lost on a host reboot. |
environment_vars | object | No | Environment variables to set in the container as key-value pairs. Keys must match ^[a-zA-Z_][a-zA-Z0-9_]*$ and be at most 128 characters long. Maximum 256 keys; each value 1 to 4096 characters. |
ssh_public_key | string | No | SSH public key for direct SSH access. Keys are globally unique (one container per key). Re-sending the same key for the same container is a no-op and completes an earlier change that did not finish. Set to null to remove the key: direct SSH stops accepting it within about a minute, an already-open session is closed shortly after, and the key becomes installable again afterwards. An empty or whitespace-only string is rejected with 400 (send null to remove). Requires a token with the ssh-keys resource permission to set or replace; removal needs only container update permission. |
comment | string | No | Optional comment for the container, max 16000 characters. Set to null to clear an existing comment. |
realm_ids | array | No | Update realm membership for this container. Containers can have different realm membership than their parent project. A signed-in session (JWT) or an unrestricted auth token can modify realm_ids; realm-restricted tokens cannot change realm membership. |
Example
Section titled “Example”curl -X PUT 'https://api.hoody.com/api/v1/containers/890abcdef12345678901cdef' \ -H 'Authorization: Bearer <token>' \ -H 'Content-Type: application/json' \ -d '{ "name": "web-app-1-renamed", "color": "#FF0000", "autostart": true, "ramdisk_scope": "container", "ramdisk": true, "environment_vars": { "NODE_ENV": "production", "LOG_LEVEL": "info" }, "comment": "Renamed after refactor" }'import { HoodyClient } from 'hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://api.hoody.com', token: process.env.HOODY_TOKEN });
const updated = await client.api.containers.update('890abcdef12345678901cdef', { name: 'web-app-1-renamed', color: '#FF0000', environment_vars: { NODE_ENV: 'production', LOG_LEVEL: 'info' }, comment: 'Renamed after refactor'});console.log(updated.data.name, updated.data.updated_at);{ "statusCode": 200, "message": "Container updated successfully", "data": { "id": "890abcdef12345678901cdef", "project_id": "67e89abc123def456789abcd", "project_alias": "Production Environment", "server_id": "111abcdef12345678901aaaa", "server_name": "node-example-1", "subserver_name": "free-1", "server": { "name": "node-example-1", "country": "SG", "country_name": "Singapore", "city": "Singapore", "region": "Asia Pacific", "datacenter": "SIN-DC1", "is_free": true, "paid_subserver": false, "specs": { "cpu_cores": null, "ram_gb": null, "disk_gb": 50, "shared_compute": true }, "expires_at": null, "kvm_supported": false, "kvm_nested": false, "kvm_checked_at": null }, "ssh_hostname": "67e89abc123def456789abcd-890abcdef12345678901cdef-ssh.node-example-1.containers.hoody.com", "env_sync": { "pending": false, "revision": 5, "applied_revision": 5, "verified_at": "2025-01-15T14:30:00.000Z", "attempted_at": "2025-01-15T14:30:00.000Z", "last_error": null, "exhausted": false }, "name": "web-app-1-renamed", "color": "#FF0000", "container_image": "debian/13", "ai": true, "hoody_kit": true, "dev_kit": true, "kvm": false, "autostart": true, "ramdisk_scope": "container", "ramdisk": true, "is_default": true, "status": "running", "environment_vars": { "NODE_ENV": "production", "LOG_LEVEL": "info" }, "volumes": {}, "ssh_public_key": "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC...", "ssh_key_fingerprint": "SHA256:wBF5vmw/82L9++9kd8PEmcEkErv5hBWXC7aJsuXAyh8", "ssh_key_installed_at": "2025-01-15T10:30:00.000Z", "comment": "Renamed after refactor", "source_container_id": null, "created_at": "2025-01-15T10:30:00.000Z", "updated_at": "2025-01-15T14:30:00.000Z", "realm_ids": [], "pool_id": null }}{ "statusCode": 400, "error": "Bad Request", "message": "Invalid container name"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
VALIDATION_ERROR | Invalid input parameters | One or more request parameters failed validation. | Check the error message for specific field requirements and correct your input. |
INVALID_ID_FORMAT | Invalid ID format | The provided ID must be a 24-character hexadecimal string. | Ensure the ID is exactly 24 characters long and contains only hexadecimal characters (0-9, a-f). |
INVALID_CONTAINER_NAME | Invalid container name | Container name must be 3 to 100 characters, alphanumeric with hyphens and underscores. | Use a valid name between 3 and 100 characters containing only a-z, A-Z, 0-9, -, and _. |
{ "statusCode": 401, "error": "Unauthorized", "message": "Authentication token required"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
MISSING_TOKEN | Authentication token missing | No authentication token was provided in the request. | Include a valid JWT token in the Authorization header as Bearer <token>. |
INVALID_TOKEN | Invalid authentication token | The provided authentication token is malformed or invalid. | Obtain a new token by logging in again or using a valid auth token. |
TOKEN_EXPIRED | Authentication token expired | The provided authentication token has expired. | Obtain a new token by logging in again or refreshing your session. |
AUTH_TOKEN_LINEAGE_REVOKED | Parent auth token no longer valid | The auth token was created by another auth token, and that token (or one above it) has been revoked, disabled or has expired, so this one no longer authenticates | Create a new auth token from a valid one, or sign in again |
EMAIL_NOT_VERIFIED | Email not verified | Returned by the login endpoint when the password is correct but the account’s email address has not been verified yet. It is only returned after the password has been checked. The response carries data.email so the client can offer a ‘resend verification email’ action without asking again. | Complete email verification by clicking the link sent to your inbox, or call /auth/resend-verification to receive a new link, or complete a password reset which also implicitly verifies the email. |
INVALID_OTP_CODE | Invalid OTP code | The provided 2FA code is incorrect or has expired | Generate a new code from your authenticator app and try again |
{ "statusCode": 403, "error": "Forbidden", "message": "Insufficient permissions"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
INSUFFICIENT_PERMISSIONS | Insufficient permissions | You do not have the required permissions to perform this action. | Ask the resource owner to grant you access. |
ACCOUNT_BANNED | Account banned | Your account has been banned and cannot access this resource. | Contact support for information about your account status. |
OPERATION_NOT_PERMITTED_ON_EXPIRED | Operation Not Permitted on Expired Container | This operation cannot be performed because the container has expired due to server termination. | The container is in a read-only state. No further operations are allowed. Create a new container. |
{ "statusCode": 404, "error": "Not Found", "message": "Container not found"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
CONTAINER_NOT_FOUND | Container not found | The requested container does not exist or you do not have permission to access it. | Verify the container ID is correct and that you have access to the project it belongs to. |
RESOURCE_NOT_FOUND | Resource not found | The requested resource does not exist or has been deleted. | Verify the resource ID and ensure it exists. |
{ "statusCode": 409, "error": "Conflict", "message": "Container name already in use within the project"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
CONTAINER_NAME_IN_USE | Container name already in use | A container with this name already exists in the project. | Choose a different name for your container. |
SSH_PUBLIC_KEY_IN_USE | SSH public key already in use | SSH public keys must be unique per container. A single public key cannot be assigned to multiple containers. | Generate a new SSH key pair for this container, or remove the key from the other container before reusing it. |
Validation Error: the request does not match the operation schema, or its JSON body cannot be parsed. message names each failing check, and data, when present, lists them one entry per failing field.
{ "statusCode": 422, "error": "Validation Error", "message": "Validation failed: /name must NOT have fewer than 3 characters"}PUT /api/v1/containers/{id}/kvm
Section titled “PUT /api/v1/containers/{id}/kvm”Also accepted: PATCH (same endpoint, identical behaviour).
Enable or disable hardware virtualization passthrough (/dev/kvm) so the container can run full virtual machines. Available on rented or dedicated (bare-metal) servers only, never on free-tier servers. The container must be stopped. Send kvm: true to enable or kvm: false to disable. dev_kvm is accepted as an alias of kvm.
The SDK reaches this same operation through two named methods:
client.api.containers.enableKvm(id)sendskvm: trueand refusesdev_kvm.client.api.containers.disableKvm(id)sendskvm: falseand refusesdev_kvm.
Parameters
Section titled “Parameters”| Name | In | Type | Required | Description |
|---|---|---|---|---|
id | path | string | Yes | Unique identifier of the container. |
Request Body
Section titled “Request Body”| Name | Type | Required | Description |
|---|---|---|---|
kvm | boolean | No | Enable (true) or disable (false) /dev/kvm passthrough (run VMs in the container). Rented or dedicated servers only; the container must be stopped. |
dev_kvm | boolean | No | Accepted alias of kvm on input. If both are sent, kvm wins and the two must agree. |
Example
Section titled “Example”curl -X PUT 'https://api.hoody.com/api/v1/containers/67e89abc123def456789abcd/kvm' \ -H 'Authorization: Bearer <token>' \ -H 'Content-Type: application/json' \ -d '{ "kvm": true }'import { HoodyClient } from 'hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://api.hoody.com', token: process.env.HOODY_TOKEN });
const result = await client.api.containers.enableKvm('67e89abc123def456789abcd');console.log(result.data.kvm);
// To disable later, call the opposite facade:// await client.api.containers.disableKvm('67e89abc123def456789abcd');{ "statusCode": 200, "message": "Container KVM setting updated", "data": { "id": "67e89abc123def456789abcd", "name": "web-app-1", "status": "stopped", "kvm": true }}{ "statusCode": 400, "error": "Bad Request", "message": "Container must be stopped"}{ "statusCode": 401, "error": "Unauthorized", "message": "Authentication token required"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
MISSING_TOKEN | Authentication token missing | No authentication token was provided in the request. | Include a valid JWT token in the Authorization header as Bearer <token>. |
INVALID_TOKEN | Invalid authentication token | The provided authentication token is malformed or invalid. | Obtain a new token by logging in again or using a valid auth token. |
TOKEN_EXPIRED | Authentication token expired | The provided authentication token has expired. | Obtain a new token by logging in again or refreshing your session. |
AUTH_TOKEN_LINEAGE_REVOKED | Parent auth token no longer valid | The auth token was created by another auth token, and that token (or one above it) has been revoked, disabled or has expired, so this one no longer authenticates | Create a new auth token from a valid one, or sign in again |
INVALID_CREDENTIALS | Invalid credentials | The provided username/email or password is incorrect. | Verify your credentials and try again, or use the password reset feature. |
EMAIL_NOT_VERIFIED | Email not verified | Returned by the login endpoint when the password is correct but the account’s email address has not been verified yet. It is only returned after the password has been checked. The response carries data.email so the client can offer a ‘resend verification email’ action without asking again. | Complete email verification by clicking the link sent to your inbox, or call /auth/resend-verification to receive a new link, or complete a password reset which also implicitly verifies the email. |
INVALID_OTP_CODE | Invalid OTP code | The provided 2FA code is incorrect or has expired | Generate a new code from your authenticator app and try again |
{ "statusCode": 403, "error": "Forbidden", "message": "KVM is not available on this server tier"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
INSUFFICIENT_PERMISSIONS | Insufficient permissions | You do not have the required permissions to perform this action. | Ask the resource owner to grant you access. |
ACCOUNT_BANNED | Account banned | Your account has been banned and cannot access this resource. | Contact support for information about your account status. |
{ "statusCode": 404, "error": "Not Found", "message": "Container not found"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
CONTAINER_NOT_FOUND | Container not found | The requested container does not exist or you do not have permission to access it. | Verify the container ID is correct and that you have access to the project it belongs to. |
RESOURCE_NOT_FOUND | Resource not found | The requested resource does not exist or has been deleted. | Verify the resource ID and ensure it exists. |
{ "statusCode": 409, "error": "Conflict", "message": "Operation cannot be performed in the container's current state"}Validation Error: the request does not match the operation schema, or its JSON body cannot be parsed. message names each failing check, and data, when present, lists them one entry per failing field.
{ "statusCode": 422, "error": "Validation Error", "message": "Validation failed: /name must NOT have fewer than 3 characters"}Deleting a container
Section titled “Deleting a container”DELETE /api/v1/containers/{id}
Section titled “DELETE /api/v1/containers/{id}”Delete a container and release its name, SSH key, and project/server quotas.
Parameters
Section titled “Parameters”| Name | In | Type | Required | Description |
|---|---|---|---|---|
id | path | string | Yes | Unique identifier of the container to delete. |
Example
Section titled “Example”curl -X DELETE 'https://api.hoody.com/api/v1/containers/67e89abc123def456789abcd' \ -H 'Authorization: Bearer <token>'import { HoodyClient } from 'hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://api.hoody.com', token: process.env.HOODY_TOKEN });
await client.api.containers.delete('67e89abc123def456789abcd');console.log('Container deletion started');{ "statusCode": 200, "message": "Container deleted successfully"}{ "statusCode": 400, "error": "Bad Request", "message": "Invalid ID format"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
INVALID_ID_FORMAT | Invalid ID format | The provided ID must be a 24-character hexadecimal string. | Ensure the ID is exactly 24 characters long and contains only hexadecimal characters (0-9, a-f). |
{ "statusCode": 401, "error": "Unauthorized", "message": "Authentication token required"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
MISSING_TOKEN | Authentication token missing | No authentication token was provided in the request. | Include a valid JWT token in the Authorization header as Bearer <token>. |
INVALID_TOKEN | Invalid authentication token | The provided authentication token is malformed or invalid. | Obtain a new token by logging in again or using a valid auth token. |
TOKEN_EXPIRED | Authentication token expired | The provided authentication token has expired. | Obtain a new token by logging in again or refreshing your session. |
AUTH_TOKEN_LINEAGE_REVOKED | Parent auth token no longer valid | The auth token was created by another auth token, and that token (or one above it) has been revoked, disabled or has expired, so this one no longer authenticates | Create a new auth token from a valid one, or sign in again |
EMAIL_NOT_VERIFIED | Email not verified | Returned by the login endpoint when the password is correct but the account’s email address has not been verified yet. It is only returned after the password has been checked. The response carries data.email so the client can offer a ‘resend verification email’ action without asking again. | Complete email verification by clicking the link sent to your inbox, or call /auth/resend-verification to receive a new link, or complete a password reset which also implicitly verifies the email. |
INVALID_OTP_CODE | Invalid OTP code | The provided 2FA code is incorrect or has expired | Generate a new code from your authenticator app and try again |
{ "statusCode": 403, "error": "Forbidden", "message": "Insufficient permissions"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
INSUFFICIENT_PERMISSIONS | Insufficient permissions | You do not have the required permissions to perform this action. | Ask the resource owner to grant you access. |
ACCOUNT_BANNED | Account banned | Your account has been banned and cannot access this resource. | Contact support for information about your account status. |
OPERATION_NOT_PERMITTED_ON_EXPIRED | Operation Not Permitted on Expired Container | This operation cannot be performed because the container has expired due to server termination. | The container is in a read-only state. No further operations are allowed. Please create a new container. |
{ "statusCode": 404, "error": "Not Found", "message": "Container not found"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
CONTAINER_NOT_FOUND | Container not found | The requested container does not exist or you do not have permission to access it. | Verify the container ID is correct and that you have access to the project it belongs to. |
{ "statusCode": 409, "error": "Conflict", "message": "Resource is currently in use"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
RESOURCE_IN_USE | Resource in use | This resource cannot be modified or deleted because it is currently in use. | Stop using the resource or remove dependent resources first. |
OPERATION_STATE_CONFLICT | Container State Conflict | The operation cannot be performed because the container is not in the correct state. | Check the container’s current status. For example, a container must be stopped to be started. |
Validation Error: the request does not match the operation schema, or its JSON body cannot be parsed. message names each failing check, and data, when present, lists them one entry per failing field.
{ "statusCode": 422, "error": "Validation Error", "message": "Validation failed: /name must NOT have fewer than 3 characters"}{ "statusCode": 500, "error": "Internal Server Error", "message": "An unexpected error occurred"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
INTERNAL_SERVER_ERROR | Internal server error | An unexpected error occurred on the server. | Try again later, or contact support if the problem persists. |
EXTERNAL_SERVICE_ERROR | External service error | A required external service is unavailable or returned an error. | Try again later when the external service is available. |